Mémoire - Romain Courpon

publicité
Université Bordeaux I
Master Info - 1ère année
Projet de Programmation 2009-2010
Dungeon Digger : Mémoire
Auteurs :
Geoffrey Boyer
Florence Brücken
Romain Courpon
Clément Jovet
Superviseur :
Philippe Narbel
Client :
Michaël Rao
1er avril 2010
Résumé
L’objet de ce projet de programmation était la reprise d’un jeu vidéo open source : Dungeon
Digger. Le développement de ce dernier était gelé depuis quelques temps et nous avons donc eu
pour tâche de l’améliorer.
Dans les grandes lignes, nous avons dû reprendre Dungeon Digger afin de le rendre plus lisible,
stable et accessible, puis lui ajouter des fonctionnalités comme une nouvelle classe de personnage,
un menu dans le jeu, et la gestion de quelques options graphiques.
Le travail fourni s’articule majoritairement autour du réusinage du code. On peut ainsi dire que
notre projet était très orienté génie logiciel.
Le projet, tel qu’il est dans son état final, permettra une reprise du code bien plus aisée qu’à
nos débuts dans Dungeon Digger, auquel on souhaite de fleurir encore.
Table des matières
1 Introduction au projet
1.1 Etude de l’existant . . . . . . . . . . . . . . . .
1.1.1 Les implémentations propriétaires . . .
1.1.2 Du côté de l’open source . . . . . . . . .
1.2 Dungeon Digger, notre projet . . . . . . . . . .
1.2.1 Contexte . . . . . . . . . . . . . . . . .
1.2.2 Ce qu’il manquait aux projets existants
1.2.3 Etat de Dungeon Digger . . . . . . . . .
1.2.4 DD en tant que PdP . . . . . . . . . . .
1.3 Les besoins . . . . . . . . . . . . . . . . . . . .
1.3.1 Besoins non fonctionnels . . . . . . . . .
1.3.2 Besoins fonctionnels . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
4
6
7
7
7
8
8
9
9
10
2 Fonctionnement du logiciel
2.1 Vue globale . . . . . . . . . . . . . . .
2.2 Principales séquences d’interactions . .
2.2.1 Lancement du serveur . . . . .
2.2.2 Lancement du client . . . . . .
2.2.3 Phase d’initiation du jeu . . . .
2.2.4 Boucle interactive . . . . . . .
2.3 Hiérarchie des fichiers . . . . . . . . .
2.3.1 Hiérarchie originale . . . . . . .
2.4 Architecture logicielle . . . . . . . . .
2.4.1 Diagramme de classes original .
2.4.2 Les défauts de l’architecture . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
12
12
15
15
16
18
20
21
21
21
21
24
.
.
.
.
.
.
.
.
.
.
.
.
25
25
25
25
28
28
29
31
32
32
33
33
34
3 Réusinage et développement
3.0.3 Hiérarchie finale . . . . . .
3.1 Architecture logicielle . . . . . . .
3.1.1 Diagramme de classes final
3.2 Processus de développement . . . .
3.2.1 Méthodologie de travail . .
3.2.2 Objectifs atteints . . . . . .
3.2.3 Problèmes rencontrés . . .
3.2.4 Outils utilisés . . . . . . . .
3.2.5 Améliorations envisagées . .
3.3 Tests . . . . . . . . . . . . . . . . .
3.3.1 Types de tests effectués . .
3.3.2 Les fuites mémoires . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
TABLE DES MATIÈRES
3.3.3
3.3.4
3.3.5
Le profilage du code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La couverture du code . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Test du serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
35
35
36
Bibliographie
36
Annexe
38
1
1.1
1.1.1
Introduction au projet
Etude de l’existant
Les implémentations propriétaires
Dungeon Keeper
Il s’agit d’un jeu de Stratégie Temps Réel datant de 1997, réalisé par Bullfrog Productions et
produit par Electronic Arts. Après un fort succès, des extensions et une suite (Dungeon Keeper
II, en 1999, avec comme apports notables le passage à la 3D et la possibilité de jouer sur Internet)
furent réalisées.
La série des Dungeon Keeper est assez célèbre pour son principe original et son ambiance
humoristique. En effet, elle met en scène un monde représenté par une très grande carte souterraine, peuplée de Keepers, de maléfiques maı̂tres de donjons, qui doivent protéger leurs antres
contre des aventuriers pilleurs de trésors, explorer et creuser la roche pour s’étendre, invoquer
des créatures de plus en plus mortelles et ainsi pouvoir partir à l’attaque des autres donjons
gérés par l’ordinateur. Des facteurs économiques (entretien financier du donjon) et stratégiques
(mélanger certains types de laquais risque d’amener à des luttes intestines) complexifient et
enrichissent le gameplay.
Un mode multijoueur offrant la possibilité de relier jusqu’à quatre joueurs maximum est
présent dans les deux opus, bien que Dungeon Keeper I ne puisse être joué qu’en réseau local.
Dans ce mode de jeu, les autres joueurs prennent la place de l’ordinateur pour gérer les donjons
adverses à conquérir.
Evil Genius et les autres
D’autres jeux similaires furent réalisés en suivant le même principe, dont Evil Genius, développé
par Elixir Studios en 2004, qui adapte la formule à un univers d’espionnage très inspiré de James
Bond et où le joueur dirige un terroriste international souhaitant arriver à la domination mondiale. Cependant, ce genre de jeu de stratégie est passé de mode, et on ne retrouve pas d’exemples
plus célèbres ni récents.
4
1.1 Etude de l’existant
5
Fig. 1.1 – Capture d’écran de Dungeon Keeper
Fig. 1.2 – Capture d’écran de Dungeon Keeper 2
6
Chapitre 1 : Introduction au projet
1.1.2
Du côté de l’open source
KeeperFX, un projet en cours
KeeperFX1 est un projet de réécriture de Dungeon Keeper actuellement dirigé par Tomazs
Lis, un ingénieur polonais. Il a pour but de modifier le jeu afin de corriger ses problèmes et de
lui ajouter du contenu amateur, d’où son nom de FX, ”Fan eXpansion”.
L’auteur se base sur les fichiers DLL et EXE de Dungeon Keeper et écrit son code à côté, son
but étant de laisser derrière lui un programme utilisable, même au cas où il l’abandonnerait en
cours de développement. Avec comme base le jeu original auquel il remplace et ajoute petit à
petit des éléments dans des modules séparés, les versions successives restent stables, jouables, et
éditables facilement.
KeeperFX, même s’il est open source, reste cependant juste une extension du jeu original, et
l’amélioration du mode multijoueur ne semble pas envisagée, deux points insatisfaisants pour
les créateurs de Dungeon Digger. Tomazs Lis cite d’ailleurs Dungeon Digger dans un article où
il est interviewé 2 .
Une multitude de projet avortés...
Il y a eu beaucoup d’essais pour cloner Dungeon Keeper, dont un certain nombre de projets
open source, mais malheureusement, comme trop souvent dans le milieu du jeu vidéo libre, la
plupart de ces tentatives ont échoué avant leur complétion. A titre d’exemples : Natural Born
Keeper, OpenDungeons, Open Keeper...
... mais une communauté persistante
Milieu février, quelques semaines après le début de ce PdP, un nouveau projet amateur fut
lancé, se nommant avec audace Dungeon Keeper 3 - War for the Overworld. Reste à voir si
Electronic Arts (propriétaire actuel de la licence Dungeon Keeper) les laissera continuer ainsi.
Le monde de grandes entreprises qu’est devenu le secteur du jeu vidéo est rarement tendre dans
ce cas de figure.
1
http://keeper.lubie.org/html/dk_keeperfx.php, dernière visite : 31 Mars 2010
KeeperFX, le prochain Dungeon Keeper 3 ?, 13 Août 2009, http://dungeon-keeper.net/, actuellement remis
à zéro pour cause de piratage, dernière visite : 31 Mars 2010
2
1.2 Dungeon Digger, notre projet
1.2
7
Dungeon Digger, notre projet
Notre Projet de Programmation, contrairement à d’autres, ne consistait pas à élaborer depuis
le commencement une nouvelle application, mais à reprendre un travail ayant débuté plusieurs
années plus tôt, le jeu vidéo multijoueur libre Dungeon Digger.
1.2.1
Contexte
Le projet Dungeon Digger fut à l’origine lancé en 2006 par deux étudiants thésards, Michaël
Rao et Thomas Pietrzak. Inspirés par l’expérience de Dungeon Keeper, un jeu vidéo consistant
à gérer un donjon et le protéger de ses envahisseurs, ils décidèrent alors de se lancer dans leur
propre œuvre.
Les premiers mois de développement furent fructueux, mais un manque d’organisation et de
planification amenèrent à une lisibilité et une stabilité insatisfaisantes du code. De plus, les
aléas de leurs vies firent que les deux créateurs durent ralentir peu à peu leur rythme de travail,
jusqu’à mettre en gel le projet.
Fig. 1.3 – Capture d’écran de Dungeon Digger
1.2.2
Ce qu’il manquait aux projets existants
L’idée originale des deux concepteurs était donc de créer eux aussi un jeu se rapprochant du
concept de Dungeon Keeper, mais avec deux grandes différences philosophiques par rapport à
leurs aı̂nés :
– Dungeon Keeper étant un jeu propriétaire au code fermé et commence à dater : il n’a ainsi
que peu évolué, et est maintenant tombé en désuétude aux yeux du grand public. Il a donc
été décidé que Dungeon Digger devait être open source, pour que la communauté puisse
travailler à l’enrichir dans le futur.
8
Chapitre 1 : Introduction au projet
– Dungeon Keeper a un mode multijoueur limité, ce que voulaient améliorer les deux créateurs
du projet. Une des idées de départ était de lui créer un univers persistant sur lequel n’importe qui pourrait se connecter à volonté et reprendre son antre dans l’état où il l’avait
laissé, ceci avec un gameplay basé sur la défense et l’invasion de donjons. En bref, faire de
ce jeu un MMORTS.
1.2.3
Etat de Dungeon Digger
Le jeu fut programmé en C++, licence GPL 2+, avec une interface graphique en SDL/OpenGL.
Leur version finale, qui devint notre version de base, contenait 18,823 lignes de code, 298 fichiers
et d’après une moyenne établie par SLOCCount, le travail requis pour la développer fut de 4.23
hommes/an.
Le projet fut mis en pause par ses créateurs en l’état que l’on peut résumer ainsi : les parties
réseau et graphique étaient quasiment achevée, le jeu pouvant être lancé, mais l’implémentation
du gameplay était tout juste commencée, y compris l’optique multijoueur avancée souhaitée par
ses concepteurs.
On trouvait trois principaux problèmes au niveau de cette version :
– Le code n’était pas propre, ni protégé, ni commenté – par exemple, pas d’accesseurs ni de
variables privées, des noms de fonctions et classes loin d’être explicites, et ainsi de suite.
La lecture des sources en était rendue difficile et peu attrayante, surtout au vu de l’absence
totale de documentation.
– Des bugs étaient présents, et certains d’une gravité importante, avec notamment des fermetures impromptues du jeu en pleine partie et des personnages ne réagissant pas face à
certains types de terrains comme les mines d’or.
– Le gameplay était minimal, voir squelettique : on pouvait lancer une partie, créer quelques
monstres et salles pour son donjon, donner des ordres, et puis ... c’est tout. De nombreuses
fonctions étaient implémentées, comme le fait que les personnages aient besoin de repos
et puissent s’entraı̂ner, mais elles n’avaient qu’un impact limité sur la manière de jouer.
1.2.4
DD en tant que PdP
C’est justement ces trois points noirs qui étaient à la base du Projet de Programmation auquel
est dédié ce document. Les consignes étaient simples : travailler sur ces défauts, rendre le code
plus lisible, augmenter la stabilité en ôtant les bugs, développer les fonctionnalités du jeu. Devant
l’ampleur de la tâche, nous avons préféré nous concentrer en plus grande partie sur le réusinage
et la correction du code, car leur priorité était supérieure à l’ajout de contenu. De plus, disposer
de bonnes bases semblait nécessaire avant de pouvoir créer quelque chose de nouveau.
Quelque chose de très important à préciser est que, malgré les souhaits originaux des concepteurs, aucune des fonctionnalités nécessaires pour un univers persistant n’étaient implémentées.
Le serveur conservait le statut de chaque joueur en cas de déconnexion durant un certain temps
(quelques minutes), mais, par exemple, il n’y avait pas de système de sauvegardes automatiques
et régulières par le serveur pour s’assurer de ne perdre aucune information en cas de crash, pas
de moyen de réellement conquérir un adversaire ... Comme ces idées n’étaient pas encore fixées
clairement, il a été décidé de mettre au second plan l’idée de persistance du monde.
1.3 Les besoins
1.3
9
Les besoins
Notre projet consistait donc à améliorer un logiciel déjà bien avancé. Afin de clarifier et
préciser nos consignes, nous avons alors établi une liste organisée des besoins fonctionnels et
non fonctionnels qu’elles engendraient. Le réusinage du code, notre action principale, impliquait
surtout des besoins non fonctionnels.
1.3.1
Besoins non fonctionnels
Besoins organisationnels
Reprise de l’existant [Extr^
eme] : Ce besoin était évident, mais il se devait d’être précisé.
Nous avions en consigne de reprendre le projet Dungeon Digger tel qu’il existait à ce moment, et
non de repartir sur une autre lancée. Nous devions donc utiliser le langage d’origine du projet,
c’est-à-dire le C++, conserver l’implémentation client / serveur actuelle et maintenir l’utilisation
des bibliothèques employées (SDL, OpenGL,...). Il comportait les sous-besoins complémentaires :
– Maintenabilité et extensibilité : Il était très probable que, après notre passage, le projet soit repris plus tard par quelqu’un - rappelons qu’il est open source. Afin de respecter
l’esprit original du projet, nous nous devions donc de nous assurer que notre architecture
soit assez souple pour permettre une évolution facile et souple.
– Application d’un standard : L’existant devait être réusiné dans un souci de cohérence
et de lisibilité. Nous avons donc appliqués le standard de programmation C++ de Google3 ,
car il est basé sur une philosophie prônant la lisibilité et le fait que n’importe qui puisse
ouvrir le code et le comprendre relativement aisément, ce qui cadrait bien à nos consignes.
De plus, Google est une entreprise ayant développé de nombreux programmes de qualité
en utilisant cette convention, et elle avait donc déjà été testée avec succès.
Tests : Faire des tests de non-régression presque à chaque modification du code, afin d’être
sûr de ne pas ajouter plus de problèmes qu’il n’y en avait déjà. Comme le champ de possibilité
des actions en jeu était plutôt mince, ces tests pouvaient être faits à la main sur les clients.
Besoins en comportement
Fiabilité [Très Haute] : Nous devions nous assurer que les bugs majeurs (ex : une erreur
de segmentation en cours de partie) soient corrigés. Les bugs moins critiques mais nuisant à
l’expérience de jeu devaient être quant à eux réduits au possible.
Performance [Haute] : Le projet est un jeu vidéo en temps réel. On se devait donc d’assurer
une performance acceptable sur plusieurs plans :
– Graphique : L’affichage à l’écran devait être suffisamment fluide, car en dessous d’un seuil
(qui varie selon les gens et les genres, mais on visait 30 images par seconde minimum) une
image animée devient saccadée et difficile à regarder.
– Réseau : Tout les clients devaient rester synchronisés, il était donc très important que
tous reçoivent les mêmes données en même temps.
3
http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml, dernière visite : 31 Mars 2010
10
Chapitre 1 : Introduction au projet
Test : Envoyer des dizaines puis des centaines de requêtes au serveur de façon continue et
mesurer le temps de réponse pour chacune des requêtes, ainsi que l’occupation en mémoire.
Même chose pour des requêtes uniques, et comparer avec les autres résultats pour évaluer
le comportement du serveur.
– Ordonnancement des tâches : Les différentes heuristiques de contrôle des troupes (gestion des déplacements, attribution d’ordres) devaient être améliorées afin que le jeu réagisse
dans l’instant suivant la commande du joueur.
Test : Mesurer le temps de traitement de l’attribution des tâches aux créatures pour
un certain nombre de tâches et de personnages. On pouvait tabler sur cinq requêtes par
seconde (peu de gens jouent aussi rapidement), par joueur, pour huit joueurs (dans les
conditions initiales de Dungeon Digger, offrir une plate-forme de jeu pour huit personnes
semblait être quelque chose d’à la fois accessible et honorable), soit quarante requêtes par
seconde. Ce seuil devait donc être un minimum à supporter.
Général : Mettre en place des tests de profil pour déterminer les points critiques à optimiser
du code - en effet, comme la performance était très importante, on risquait de devoir faire
quelques concessions à certains endroits critiques et y garder un code ”sale”, par exemple en
gardant certaines fonctions cruciales dans des fichiers headers afin qu’elles soient optimisées par
le compilateur, même si cet acte est habituellement déconseillé.
Remarque : Un client texte vieux de plusieurs versions était fourni avec Dungeon Digger, avec
possibilité de le remettre à jour, afin de pouvoir faire des tests de performance du code, interface
graphique mise à part.
Besoins externes
Contraintes d’interopérabilité [Moyenne] : Il est à noter que les implémentations propriétaires que nous avons trouvés étaient toutes exécutables uniquement sur un système MSDOS ou Windows, ce qui n’était pas le souhait des créateurs de Dungeon Digger. Le code devait
donc pouvoir être compilé et exécuté sur les systèmes d’exploitation Linux et Windows. Un
grand effort avait déjà été fait et ce sens.
Contraintes légales : Un jeu vidéo requiert de nombreuses ressources graphiques 2D, 3D et
sonores. Le projet étant open source, il fallait que le contenu distribué ne brise aucune conditions
d’emploi et soit libre de droits. Cette contrainte était bien entendu déjà respectée et il nous fallait
la maintenir dans le cas d’ajout de ressources.
1.3.2
Besoins fonctionnels
Contrôle du client par le serveur [Haute] : Le projet étant open source, il est impossible
de savoir si le client utilisé est exactement le même que celui officiel. Il fallait donc limiter au
maximum les possibilités qu’un joueur puisse tricher en modifiant son client. A l’origine, la
situation était bien gérée, à l’exception des collisions : le serveur ne vérifiait en effet pas si le
déplacement d’un personnage était possible, et il y avait donc la possibilité qu’un client modifié
pour faire passer ses personnages à travers les murs envoie des messages brisant cette règle.
Modifier le serveur pour tester les collisions était donc un ajout potentiel intéressant.
Test : Fabriquer un client spécial, aux valeurs modifiées pour avantager le joueur (plus d’or au
départ, vitesse de mouvement des troupes augmentée, temps de repos nécessaire diminué pour
les troupes, capacité de passer à travers d’autre entités), et envoyer des requêtes au serveur afin
de voir s’il les ignorait comme il le devait.
1.3 Les besoins
11
Fonction de sauvegarde en temps réel d’une partie [Faible] : Afin de permettre une
utilisation continue (par exemple, reprendre sa partie après un crash, qu’il vienne du client ou
du serveur), une fonction permettant d’enregistrer les données de la partie en permanence et
en temps réel dans un fichier unique aurait pu être créée afin de s’assurer qu’à sa lecture et
son chargement, on puisse reprendre là où on s’en était arrêté. Un jeu multijoueur se passe
d’habitude très bien d’une fonctionnalité similaire, mais cette fonctionnalité aurait été dans le
sens originel des créateurs.
Brouillard de guerre [Faible] : Savoir ce que font les autres joueurs est, par le biais d’espions ou d’éclaireurs, une des bases des jeux de stratégie. Il est important alors qu’on ne puisse
pas observer leurs positions en déplaçant simplement la caméra. Dungeon Digger implantait déjà
ce concept, mais il pouvait être amélioré d’un point de vue programmation et interaction.
2
Fonctionnement du logiciel
Dungeon Digger est composé de 2 programmes (obtenus en compilant le code grâce à la
commande make) :
– digger-server, le serveur de jeu ;
– digger, le client.
C’est tout ce qu’il suffit de faire pour jouer au jeu, à condition qu’on ait les librairies nécessaires
(la liste est présente dans le fichier README à la racine du dossier code). Il suffit alors juste
de lancer un serveur puis autant de clients que nécessaire avec les commandes standards du
système d’exploitation.
L’architecture du logiciel, importante pour comprendre comment est codé le jeu, sera traitée à
part entière dans la partie suivante car elle a subit des modifications. En effet, ce qui est présenté
ici reste valable après notre travail puisque nous ne sommes pas tellement intervenus dessus.
2.1
Vue globale
Voici un diagramme d’états-transitions résumant les actions/évènements du serveur lorsqu’un
client se connecte avec lui.
Fig. 2.1 – Comportement du serveur
12
2.1 Vue globale
13
Fig. 2.2 – Les affreux hommes rouges lancent le serveur...
Les deux diagrammes de cas d’utilisation illustrent brièvement le fonctionnement interne du
logiciel, côté serveur puis côté client. Comme on peut le voir, le serveur s’initialise, se met en
écoute des clients, reçoit leurs informations et les retransmet ensuite à tous. Le client s’initialise
en demandant au serveur des informations comme la carte commune, et ensuite le joueur peut
gérer son donjon !
14
Chapitre 2 : Fonctionnement du logiciel
Fig. 2.3 – ... et s’y connectent pour y jouer.
2.2 Principales séquences d’interactions
2.2
15
Principales séquences d’interactions
Pour aider à la compréhension de l’architecture du programme, nous avons créés quelques
diagrammes de séquences. Ils présentent les phases d’initialisation du serveur et du client de jeu,
ainsi que la boucle gérant les évènements (souris et clavier) sur ce dernier.
2.2.1
Lancement du serveur
Les fonctions initobjects(), initbuilding(), initbuddies() et initactions() initialisent le serveur
afin de permettre d’utiliser les différentes entités du jeu et de les faire interagir entre elles. Par
exemple, initbuddies (les buddies étant les unités : guerriers, ouvriers, chercheurs) créé ces trois
classes de personnages, qui seront ensuite instanciées individuellement à chaque fois que le joueur
produira ces laquais.
Une fois prêt, il créé la carte qui sera commune à tous les joueurs (classe map t), et il signale
qu’on est du côté serveur (les clients ont chacun leur propre copie locale de la carte et communiquent au serveur les changements de leurs côtés, qu’il distribue ensuite à tout le monde).
L’envoi du signal process(0.) signifie qu’on doit commencer à ”traiter” les unités, bâtiments et
autres entités, et ceci immédiatement (0. est ici le délai de traitement, il peut être déclaré avec
une autre valeur dans d’autres cas de figure).
Enfin, une fois la carte prête et lancée , on ouvre le GameServer en lui précisant le port
afin qu’il se mette en écoute dessus, prêt à recevoir les demandes des clients, les traiter et les
16
Chapitre 2 : Fonctionnement du logiciel
redistribuer.
2.2.2
Lancement du client
2.2 Principales séquences d’interactions
17
C’est ici l’initialisation de l’interface graphique, qui se fait donc du côté client.
D’abord, le programme se lance, et créé une instance de LuaMachine pour traiter les divers
scripts du jeu. Celle-ci est embryonnaire, mais permet néanmoins déjà de charger la configuration
du jeu (située dans le fichier scripts/config.lua) et sa langue (des fichiers lua à part stockent toutes
les variables textes du jeu, permettant de les traduire aisément). On lance ensuite un Screen,
c’est à dire un écran sur lequel on va pouvoir manipuler la souris, puis un MainMenu pour le
remplir (voir plus bas). On place ce menu en ”racine” (setRoot(bg)) de notre écran, et on se met
ensuite en attente d’évènements à traiter (mainLoop()).
Au sujet de DEV VERSION : il s’agit d’une constante par défaut à 0, ce qui fait qu’on n’entre
jamais dans les if montrés sur le schéma, uniquement dans les else. Cette valeur correspond à
un enchaı̂nement différent des actions : en effet, actuellement (c’est à dire pour un paramètre à
0 et empruntant la voie des else), le MainMenu lance le menu principal du jeu, qui permet de
choisir un jeu en local, sur Internet ou d’accéder aux options. Ensuite, une fois le choix fait, ce
MainMenu lance un MainBackground avec les informations nécessaires à la connexion (dont IP
et port du serveur). C’est ce background qui représente l’interface graphique du jeu, avec carte,
sons, etc.
Si DEV VERSION était à 1, on sauterait le MainMenu pour envoyer directement ces informations au MainBackground et lancer ainsi immédiatement un jeu en mode local. Il s’agit donc
en fait d’une variable pour les développeurs souhaitant arriver en jeu directement pour tester
plutôt que de passer par l’étape des menus à chaque lancement.
18
2.2.3
Chapitre 2 : Fonctionnement du logiciel
Phase d’initiation du jeu
2.2 Principales séquences d’interactions
19
Le MainBackground est le même que l’on peut voir dans le lancement du client. Il s’initialise
comme le serveur (voir plus haut) au début, car il va lancer une copie locale de la carte commune
et il doit donc être prêt à recevoir et manipuler les informations correspondantes, en lui précisant
l’identification du joueur auquel elles sont liées. Ensuite, on se connecte et se synchronise avec
le serveur (voir connection et sa note, qui fait le rôle de lien entre le client et le serveur).
L’initialisation des composants graphiques est très longue et il n’est guère utile de la détailler
en entier, on se contente donc de déclarer qu’elle est présente. Elle consiste notamment à charger
toutes les sources 3D (chaque type de modèles : unités, meubles, objets), les textures (FloorCellDrawer, WaterCellDrawer, GroundCellDrawer, et d’autres), les sons, et ainsi de suite.
20
2.2.4
Chapitre 2 : Fonctionnement du logiciel
Boucle interactive
Il s’agit du même Screen que l’on peut voir dans le lancement du client. Il fait tourner la boucle
principale mainLoop() en permanence, et ordonne de traiter les évènements (Events) détectés
par le logiciel. A chaque fois que l’un d’entre eux se produit, on l’identifie puis le décompose afin
de savoir si par exemple c’est un mouvement de la souris ou du clavier, en quel sens il a été fait
(bas ou haut ?), puis on le transmet au Widget de gestion qui se charge de faire apparaı̂tre le
résultat à l’écran.
2.3 Hiérarchie des fichiers
2.3
21
Hiérarchie des fichiers
Le réorganisation des fichiers d’un programme peut sembler être une tâche trop mineure
pour être citée, mais le répertoire de Dungeon Digger comprend au moment de sa remise un
peu plus de 300 fichiers, qui étaient à l’origine répartis d’une manière contestable. Une bonne
réorganisation de ceux-ci était donc une des fondations pour un réusinage réussi.
2.3.1
Hiérarchie originale
La hiérarchie de base semblait convaincante : nous avions à la racine du répertoire plusieurs
dossiers comportant respectivement les fichiers binaires exécutables, les données de jeu, les headers, les scripts de configuration et les sources.
Mais en ce qui concernait les headers et les sources, l’organisation n’était pas uniforme :
nous pouvions décomposer le code en trois parties, deux pour les mécanismes du jeu et la
gestion réseau, dont les headers et sources étaient mélangées dans des dossiers qui leurs étaient
respectifs, et une autre pour l’interface graphique dont les headers et sources étaient séparées,
comme présenté dans l’arborescence ci-dessous. Dungeon Digger fut à l’origine développé par
deux personnes utilisant des méthodes d’organisation différentes, ce qui explique cette différence,
chacun ayant géré son code selon son goût.
Il est à noter que les noms de dossiers, sources et données sont en anglais, conformément à
l’esprit open source du projet.
2.4
2.4.1
Architecture logicielle
Diagramme de classes original
Le code dans son état original était dense et donc dur à assimiler en intégralité, et le diagramme
de classes qui en découlait, complexe, a dû subir quelques retraits d’informations nécessaires :
toutes les liaisons ne sont pas représentables.
22
Chapitre 2 : Fonctionnement du logiciel
Comme le diagramme est grand, on a coloré les lignes des éléments selon leur positions dans
le système de fichier, car c’est ce qui ressemble le plus à une organisation en paquetages :
– vert : dossier src/net/,
– bleu et violet : dossier src/game/
– le violet correspond à la hiérarchie des classes représentant des objets qui sont modifiés
au cours du temps, ou autrement dit, les classes descendant de updatable t
– le bleu clair correspond aux classes servant à stocker des données générales à des ”genres”
d’objets, par exemple buddy class t est instanciée une seule et unique fois pour chaque
genre de buddy t : guerrier, travailleur, etc.
– le bleu foncé est donné au reste des classes/fichiers de src/game/
– rouge : dossiers include/ et src/
On a laissé assez floue la partie concernant l’interface car elle n’était pas une priorité dans le
réusinage du code et son amélioration.
2.4 Architecture logicielle
23
24
2.4.2
Chapitre 2 : Fonctionnement du logiciel
Les défauts de l’architecture
Un mauvais nommage
Dans l’architecture originale, la plus grande confusion régnait quant au nommage des classes :
en effet, selon qui des deux créateurs les avaient faites, elles portaient des noms très différents, et
la plupart du temps, ni très clairs, ni très évocateurs. En exemple, cobject t voulait en fait dire
cell object, un objet utilisé sur une cellule, une case de la carte. Dans le gameplay, il s’agissait
donc d’un meuble : difficilement devinable à son nom ! Toutes les entités du répertoire game
avaient de plus une convention de nommage différente du reste du programme (tendance à se
nommer xxx t).
L’orienté objet trop délaissé
Il y a un aussi un problème qui lui, concerne l’orientation objet du code. En effet, le code,
même s’il est taggé C++, comporte pas mal de parties notamment qui ne sont pas du tout objet
et qui ne tirent pas parti de l’héritage. En effet, l’élément principal qui serait à refaire, est
l’implémentation de certaines classes qui contiennent un flag ou un pointeur vers un objet qui
décrit leur spécificités.
Par là, on veut dire que ces classes sont instanciées de manière identique, seulement, on peut
les différencier et ”leur” appliquer des actions données liées à ce champ. Ce dernier est donc
comme une clé (on pense ici aux bases de données) permettant de les étendre.
Un exemple est la classe buddy t. Ses instances représentent les ”bonshommes” du jeu. Elles
se ressemblent toutes à part qu’elles contiennent un enum tid qui fait référence à une instance
particulière de buddy class t.
En effet, au démarrage du jeu, une instance de la classe buddy class t est créée pour chaque
sorte de ”bonhomme” : à l’origine, le travailleur et le guerrier. Ensuite, pour chaque ”bonhomme”, une instance de buddy t est créée. Si le joueur a demandé un guerrier alors, le champ
tid est mis à la valeur représentant le guerrier. C’est lorsque le joueur commande des actions
que le répartiteur des tâches (tasker t) va se servir de ce champ (indirectement).
3
Réusinage et développement
3.0.3
Hiérarchie finale
Nous avons détecté un problème d’uniformisation au niveau de la localisation des headers
et des sources, nous avons donc modifiés l’organisation de ces derniers en conséquence, en les
séparant avec un dossier respectif pour chaque partie du programme. Les autres fichiers n’ont
pas été déplacés. Cette nouvelle hiérarchie est simple de compréhension et d’utilisation.
3.1
3.1.1
Architecture logicielle
Diagramme de classes final
Les modifications ont majoritairement été faites dans le répertoire game du code, séparé de
l’interface graphique et de la partie réseau, car il a été écrit par Michaël Rao, le client du
projet. Le découpage original du code était relativement correct, et donc du point de vue des
diagrammes de classes, on peut noter qu’il y a juste eu un grand renommage pour augmenter
sa clarté globale. Il est possible de lire la section 3.2.5, ”Améliorations envisagées” pour plus
d’information à ce sujet.
Comme on peut le voir, il a été choisi de renommer toutes les ” class” en ”Kind”, afin de
faciliter leur compréhension. Ainsi, nous avons entre autres BuddyKind et BuildingKind. Anecdote, toutes les références à des area (zones, en anglais) étaient dans le code original des aera
(air, aéroport, etc.). Cette confusion linguistique a évidemment été corrigée. La classe race
25
26
Chapitre 3 : Réusinage et développement
apparaı̂t dans notre diagramme mais se limite à la déclaration de quelques variables. Elle est
pour l’instant inutile mais est conservée pour d’éventuelles et lointaines modifications du jeu qui
permettraient d’avoir plusieurs races jouables.
Un des avantages de cette mise à jour de l’architecture du logiciel est la nouvelle clarté qui
en découle. Avec ces nouveaux noms de classes bien plus explicites, on se passe presque de
commentaires dans le code dès lors que l’on s’efforce de les peupler de fonctions avec des noms
eux aussi parlants.
Un autre point important qui n’apparaı̂t pas dans le diagramme : le découpage entre fichiers
sources et headers. En effet, il est très fortement conseillé (voir obligé) selon les standards
de programmation du C++ de séparer proprement le code entre ces deux fichiers, avec les
déclarations de fonctions dans les headers puis leur corps dans les sources. Cette convention
n’était pas respectée originalement, et nous l’avons donc appliquée de notre mieux.
3.1 Architecture logicielle
27
28
3.2
3.2.1
Chapitre 3 : Réusinage et développement
Processus de développement
Méthodologie de travail
L’organisation du projet fut légèrement chaotique. Bien que le groupe soit parti avec une
légère longueur d’avance (le client avait déjà été rencontré, avec la possibilité de se lancer dans
le programme, en décembre), nous n’avons pas su tirer entièrement profit du temps qui nous a
été imparti.
Par exemple, il nous a été demandé tôt dans le projet d’établir une liste des besoins fonctionnels
et non fonctionnels à appliquer à notre projet. Pour les autres groupes, cette liste était à créer
entièrement suite à de longues réflexions, alors que nous aurions dû pour notre part en profiter
pour commencer à nous familiariser avec le code et à en découvrir ses failles. Nous avons imités
les autres, alors que nous aurions pu y gagner quelques précieuses semaines, surtout que nous
n’avons finalement pas eu le temps de nous pencher sur un certain nombre de ses besoins.
Il fut décidé aux débuts du projet de ne pas nommer de chef d’équipe, en partant du principe
que nous étions quatre étudiants motivés et déjà ”triés” dès la création du groupe. Ainsi, nous
espérions avoir une certaine liberté dans notre travail qui nous permettrait de prendre des
initiatives intéressantes. En rétrospective, cette décision à en effet permis les initiatives, mais
a aussi retardé certains débats qui auraient pu être coupés court avec une voix unique pour
confirmer ou infirmer définitivement quelque chose.
Nous avons beaucoup appris quant au travail en groupe. Par exemple, la plupart de nos
débats se tenaient avant sur le forum de notre projet, et prenaient beaucoup de temps pour peu
de décisions. Lors des vacances d’hiver, face à l’avancement faible du projet, il fut décidé de
revoir notre fonctionnement et de procéder de manière un peu plus professionnelle, notamment
par des réunions en face-à-face que nous avons tenus régulièrement par la suite.
Une autre décision prise fut de concentrer certains d’entre nous sur certaines parties du projet
comme le réusinage, d’autres sur le mémoire et d’autres sur une amélioration sensible de l’interface du jeu (menu, options). En effet, la partie ”interface” (voir le dossier du même nom) du
jeu ayant peu d’interactions avec son noyau (voir dossier game), plutôt que d’attendre la fin du
réusinage de ce dernier, nous avons préféré essayer de proposer aussi nos propres évolutions.
3.2 Processus de développement
29
Plannings
L’équipe chargée du projet fut composée de quatre personnes, avec un planning étalé sur
douze semaines. Le travail a commencé la deuxième semaine du semestre, pour s’achever le 2
Avril 2010.
Planning prévisionnel :
Planning réel :
3.2.2
Objectifs atteints
Nous avons réussis à traiter les besoins non fonctionnels tels qu’ils étaient décrits plus haut
dans ce document, bien que le réusinage n’ait pas pu être fait à 100% par manque de temps et que
par conséquent nous n’avons pas pu nous pencher sur l’optimisation du jeu (ces optimisations
étaient à faire une fois le réusinage fini afin de s’assurer que les performances ne soient pas
inférieures au jeu original).
Le premier succès, qui pourrait paraı̂tre négligeable, est d’avoir réuni dans une nouvelle architecture de fichiers toutes les données nécessaires au lancement et à l’évolution du jeu. En effet,
au début nous avions la version du serveur svn sourceforge de l’équipe originale, celle stockée par
M. Rao, et étant à une version différente, un pack de données supplémentaires contenant sons
et textures manquait alors qu’il aurait du être présent, nous l’avons donc inclus à notre dépôt.
La partie de ce mémoire consacré à l’architecture du Logiciel, section Hiérarchie des Fichiers
décrit également les disparités internes à la structure du programme. Toutes les données sont
désormais stockées et triées sur un serveur unique et facile d’accès.
30
Chapitre 3 : Réusinage et développement
Ensuite, les classes du programmes ont été renommées afin de rendre l’architecture et les
interactions internes bien plus claires, comme on peut le voir avec les diagrammes de classes
de la partie Architecture logicielle. Des commentaires ont aussi été ajoutés dans le code suite
à nos séances avec M. Rao, et grâce à l’application d’un standard de programmation régulier,
comprendre la structure interne pour y ajouter des nouveaux éléments est bien plus aisé - c’est
ce qui nous à par exemple permis de créer un nouveau type de buddy, le Chercheur, qui se rend
aux bibliothèques du donjon pour y étudier. L’implémentation du chercheur passe par la création
d’un nouveau buddy, avec un flag INTRA BUDDY RESEARCHER passé en paramètre permettant au
gestionnaire des tâches de lui attribuer le rôle attendu. Le chercheur réside donc sur ce flag,
pour lequel nous avons dû écrire quelques nouvelles fonctions.
Dans les nouveautés, il est à noter qu’un menu est maintenant accessible dans le jeu, proposant
trois alternatives, soit retourner au jeu, ce qui ferme le menu, soit accéder aux options, ce
qui permet de modifier certaines options graphiques, soit quitter le jeu. En ce qui concerne la
gestion des options, les fichiers /interface/OptionsWindow.* ont été entièrement réécrits par
nos soins, ils ne proposaient de base que l’affichage de simples boutons sans aucun traitement.
L’implémentation de ces nouveautés a nécessité la création de plusieurs nouvelles classes qui
héritent de classes existantes pour redéfinir certains événements, comme l’ouverture et l’écriture
d’un fichier pour enregistrer les modifications. Actuellement, il n’est possible que de changer le
mode d’affichage (fenêtré / plein écran) et la résolution (à l’aide d’une liste présentant toutes les
résolutions disponibles, celle sélectionnée de base étant la résolution actuelle), ces modifications
s’enregistrent instantanément dans le fichier de configuration /scripts/config.lua.
Ces ajouts ne rentrent pas réellement dans le cadre de notre projet, mais ces fonctionnalités sont primordiales dans un jeu de nos jours. Ils peuvent paraı̂tre triviaux, mais il a fallu
énormément de temps pour s’approprier le code et arriver à ce résultat.
D’autres ajouts et réglages ont été faits, afin que la réécriture du code ne soit pas notre seule
action :
– Tout les avertissements à la compilation ont été vérifiés et réglés ;
– Tout les fichiers ont été mis à jour pour fonctionner avec les dernières versions des librairies
utilisées et ont été testés sur des machines avec divers types d’installation ;
– La documentation a été mise à jour où créée de façon interne selon la situation. Des
fonctions qui étaient incomplètes sont maintenant clairement marquées comme tel.
– Certaines textures de boutons du menu étaient simplistes ou absentes, nous les avons
(re)faites.
– Un thème musical correspondant à l’identité du jeu a été composé, il tourne en boucle
tout au long de la partie.
Concrètement, tout cela veut dire que n’importe qui pourrait maintenant se connecter au
serveur svn du projet, le télécharger en intégralité en quelques commandes, lire notre documentation et installer les paquetages recommandés, puis enfin lancer le jeu - où bien se mettre à
l’éditer sans aide extérieure, ce qui est tout à fait dans l’optique open source du projet. Nous
espérons que les transformations effectuées serviront et aideront à encore améliorer le jeu.
3.2 Processus de développement
3.2.3
31
Problèmes rencontrés
Au niveau du réusinage
Le réusinage était l’objectif principal de notre projet, celui qui était défini par le client comme
un des plus importants. Il s’est par la suite révélé être une tâche très sensible.
Rappelons brièvement quelques détails sur Dungeon Digger - il s’agit d’un jeu né plusieurs
années avant notre Projet de Programmation, écrit par un programmeur expérimenté, son code
étant complexe et éparpillé dans des dizaines de fichiers. Comme on peut le voir à son architecture, il y a énormément de classes, entrecroisées de manière très fine, et à certains endroits le
simple fait de renommer une fonction pouvait créer des dysfonctionnements curieux en jeu.
Ainsi, pour prendre un exemple quant à cette sensibilité, le fait que le projet soit programmé
de façon objet fait que de nombreuses fonctions et paramètres sont hérités ou partagés entre
les classes, et que les renommer peut leur faire perdre le lien avec leurs ancêtres. Dans le cas
de fonctions, elles sont comptées comme nouvelles plutôt que redéfinissant des anciennes, et
bien qu’elles ne lèvent pas d’avertissements, leur comportement en jeu devient soudainement
incohérent. Un bon exemple est le fichier buddy.h, fourni en annexe : la plupart des fonctions
hors accesseurs sont héritées, et il a fallu un certain travail pour le deviner, remonter à leurs
sources et les éditer.
Même en dehors du renommage, l’héritage peut aussi rendre certaines choses compliquées.
Dans le cas de pointeur sur une liste d’objets qui n’est pas forcément définie précisément dans
le code, on se retrouve par exemple avec des
(*p)->fonction() (où p est un pointeur quelconque)
qui ne sont pas reconnues par l’outil de réusinage, et plus particulièrement si cette fonction()
est présente avec le même nom dans des classes n’ayant rien à voir.
Le temps ainsi pris par toutes ces recherches et ces tests a malheureusement rogné sur le temps
disponible pour s’atteler aux autres besoins du projet, mais cela était prévisible.
Au niveau des tests
Certains besoins initialement prévus dans le projet n’ont pas été traités faute de temps, nous
n’avons par conséquent pas élaboré les tests les concernant.
Dans une situation plus classique, une batterie de tests aurait pu permettre de fluidifier ce
processus, mais quelques faits ont gênés le processus :
– L’absence de tests avec les sources déjà fournies ;
– L’absence de commentaires explicites dans le code ;
– L’architecture compliquée du logiciel (voir diagrammes de classe) ;
– La simple quantité des fonctions déjà présentes.
Écrire des tests unitaires dans ces conditions était quelque chose de très difficile. Par exemple,
même dans le cas d’une fonction que l’on testait et qui retournait un booléen VRAI, la simple
difficulté de compréhension de son utilité ne permettait pas de savoir si ce booléen était un
résultat valide.
32
Chapitre 3 : Réusinage et développement
Pour palier à ce fait, nous testions à chaque modification le jeu lui-même, à la main, ce qui
n’était pas si long au vu des possibilités réduites d’actions à y faire.
Un autre point est la remise à jour du client texte dont nous avons parlés dans la définition
des besoins. Il s’est révélé que celui-ci n’était pas seulement ”pas à jour”, il était en retard de
plusieurs versions majeures, et complètement incompatible avec le programme principal. Il aurait
dans notre cas fallu le réécrire entièrement, en partant du client graphique et en le modifiant,
mais le temps nous a gravement manqué pour nous en occuper.
3.2.4
Outils utilisés
Le projet a nécessité l’emploi du module C++ de l’environnement de développement Eclipse1 ,
permettant en quelques clics d’avoir accès à toutes les occurrences d’une variable, d’une classe ou
d’une fonction sélectionnée, utilité cruciale dans un processus de réusinage. En effet, à chaque
renommage d’entité, le module permettait de savoir où cette entité était utilisée afin de l’y
éditer aussi et de rendre le travail plus fluide. Attention, que la présence de cet outil ne donne
pas l’impression que le réusinage en devenait une tâche aisée et rapide. Il le rendait simplement
possible. De nombreuses variables et fonctions partageaient le même nom et requéraient donc
que l’on comprenne les finesses du code.
Dungeon Digger n’aurait pu être mené à point sans la plate-forme Savane proposée par le
CREMI. Le versionnage des données, ainsi que la fonction de comparaison entre les différentes
versions d’un fichier, a permis de rattraper beaucoup d’erreurs apportées involontairement dans
le code, tout en étant un outil simple d’emploi pour le travail en équipe.
Le programme avait des problèmes de fuites mémoires, et nous avons donc utilisé valgrind
pour essayer de comprendre d’où venait exactement le problème.
3.2.5
Améliorations envisagées
Comme expliqué dans la partie du mémoire présentant les défauts de l’architecture du logiciel,
certains endroits du code ne sont pas vraiment orienté objet. Les auteurs originaux ont préféré
avoir un jeu jouable rapidement plutôt qu’un code bien écrit, et l’utilisation de switchs en C++
produit moins de code qu’introduire une ou plusieurs nouvelle(s) classe(s).
Prenons le cas des buddies, il pourrait être intéressant d’implémenter une classe abstraite
Buddy qui permettrait de définir un nouveau type, et ensuite de créer autant de classes qui en
héritent que de genre de buddies présent dans le jeu, et chacune de ces classes filles redéfinirait
et implémenterait les morceaux de code propre au rôle du genre concerné. Le type réel serait
donc affecté au moment de l’exécution conformément au constructeur de chacune de ces classes.
Ce genre d’amélioration requiert de reprendre entièrement le code du jeu, et en particulier
nécessite une refonte complète du répartiteur des tâches (tasker), c’est la raison pour laquelle
nous n’avons pas eu le temps d’envisager la mise en place d’une telle implémentation.
1
Une version d’Eclipse C/C++ est disponible à l’adresse http://www.eclipse.org/downloads/download.php?
file=/technology/epp/downloads/release/galileo/SR2/eclipse-cpp-galileo-SR2-linux-gtk.tar.gz
3.3 Tests
3.3
3.3.1
33
Tests
Types de tests effectués
Comme expliqué précédemment, nous n’avons pas réalisé de test unitaire, mais nous avons
entrepris de faire le maximum de tests concernant l’analyse des performances de notre application, afin de nous assurer leur non-régression. Trois outils nous ont été particulièrement utiles
dans cette tâche :
Valgrind
Valgrind est un outil complet qui permet une analyse extrêmement détaillée de l’utilisation
de la mémoire par un programme, et donc notamment tous les problèmes engendrés par une
mauvaise programmation de celui-ci. Nous l’avons utilisé avec la ligne de commande suivante :
valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --num-callers=20
--track-fds=yes --log-file=results ./digger
Valgrind étant un programme modulaire, nous avons fait nos tests avec le module Memtest,
qui permet la détection de fuites mémoire ainsi que plusieurs autres points que nous détaillerons
dans la partie suivante.
GProf
GProf est un outil de profilage, sa fonction étant de déterminer le temps processeur utilisé
par chaque fonction d’un programme, ainsi que le nombre d’appels à celle-ci, et aussi le nombre
de fois où cette même fonction a été appelée par d’autres fonctions du programme.
L’utilisation de GProf est relativement simple : dans notre cas, nous avons ajouté l’option
-pg lors de la compilation avec gcc. A l’exécution de digger, un fichier gmon.out est crée. Le
résultat est ensuite visualisable avec la commande :
gprof -p -q digger
Gcov
Gcov permet de tester la couverture du code. Ainsi, nous pouvons savoir combien de fois
une ligne de code spécifique est exécutée, et aussi quelles lignes sont exécutées. Cela permet de
repérer les parties utiles du code plus rapidement, ainsi que les optimisations effectuées et leurs
effets.
Le programme s’utilise aussi de concert avec gcc. Il suffit d’ajouter en compilation l’option -fprofile-arcs -ftest-coverage, et d’exécuter dans notre cas digger. Le résultat de
l’exécution est placé dans un fichier *.gcda pour chaque fichier contenant du code exécuté, et
peut être analysé en lançant :
gcov *.gcda
Il est à noter que vu la taille importante des logs de chacun des tests, ils ne sont pas fournis
entièrement en annexe, mais en partie. Les résultats complets se trouvent sur le dépôt du projet.
34
3.3.2
Chapitre 3 : Réusinage et développement
Les fuites mémoires
Nous avons commencé à regarder l’aspect “utilisation mémoire” de notre projet quand nous
avons eu certains messages d’erreur lors de l’exécution, faisant penser à une fuite mémoire. Les
conditions de test sont les suivantes : nous avons lancé digger et nous avons fermé l’application
par le bouton Quitter du menu principal. Valgrind réduisant les performances de l’application
par un facteur de 20 à 30 fois, il est impossible d’essayer de quitter le jeu depuis le menu de
l’interface. Néanmoins, le même test a été réalisé en quittant par le bouton de fermeture de la
fenêtre. Dans les deux cas de test, les résultats étaient identiques et surprenants, montrant des
fuites importantes, comme en témoigne le résumé de Valgrind :
Le log de Valgrind montre bien en détail chaque objet non désalloué, avec son placement dans
le code, ainsi que la taille de l’objet. Ceci montre bien que le projet original faisait abstraction
des problèmes de libération de la mémoire. Voici un exemple de présentation des objets non
libérés sous Valgrind :
La plus grosse perte de mémoire a lieu dans les composants utilisant la librairie SDL, tels que
la gestion de la fenêtre graphique (SDL SetVideoMode), le chargement de texture (IMG Load),
la création de surface (SDL CreateRGBSurface), ainsi que la gestion de lecture du format MP3
(MPEG ring)
Nous avons bien sûr essayé de résoudre ce problème, en trouvant l’endroit de définition des
objets, et en créant des fonctions de sortie (Quit). Ceci n’a malheureusement pas été très efficace,
du fait de nouveaux problèmes apportés (crash dans le jeu, problème de “double libération”).
La difficulté majeure est aussi de savoir où et quand libérer certains objets, qui deviennent
inaccessibles quand un pointeur change d’adresse (notamment le cas de setRoot()).
Il s’avère, après certaines vérifications, que le problème de libération des objets SDL est justifié.
En effet, certaines bibliothèques, comme la SDL, allouent de la mémoire qui ne peut pas être
libérée à la fin de l’exécution. Ce “problème” de mémoire est même présent dans certaines
implémentations de la bibliothèque standard. Il est justifié par des optimisations de code. Dans
le cas de SDL, la fonction SDL Quit() permet de libérer la mémoire de tous les sous-systèmes
de la SDL. Elle doit être appelée à la fin du programme. L’appel à cette fonction était absent
dans la première implémentation du projet, nous l’avons donc ajouté. Malgré cela, Valgrind
détecte toujours les mêmes fuites... En résumé, il est possible que Valgrind détecte mal une
3.3 Tests
35
optimisation et déclare la mémoire de l’objet en question comme perdue. Ce problème trouvera
peut-être une solution dans une implémentation future de Valgrind.
La solution d’utiliser un ramasse-miettes a été rapidement envisagée, mais s’avère difficilement possible, car elle grèverait les performances de façon trop importante, et rendrait le jeu
injouable. Il est donc important dans le futur d’apporter une solution à ce problème, ce qui
devra probablement passer par un remaniement dans la déclaration des variables.
3.3.3
Le profilage du code
Le profilage est une étape importante dans nos tests car notre but essentiel est la performance,
et donc vérifier la non-régression par rapport à l’original. En effet, le jeu doit tourner aussi bien
que possible, et ceci implique une efficacité particulière du code, notamment dans les algorithmes
de parcours et le dessin de la fenêtre 3D. Les résultats du test effectué montre bien ce point :
On voit très clairement que le dessin des éléments 3D, effectué par la fonction drawRectangle,
est l’élément principal utilisant le plus de ressources. C’est tout à fait logique dans notre cadre,
et une amélioration des performances est peu probable, le système de gestion de l’affichage étant
géré avec SDL.
L’autre élément consommateur principal de ressources est le rafraı̂chissement du feu (fonction
update de la classe Fire), qui se charge de calculer la position des particules composant le feu
à un moment précis. Il est possible qu’une amélioration soit faite sur cette fonction, composé
d’un test if - else, mais ceci provenant de la gestion de l’interface, nous n’avons pas touché à
ce code, et ainsi, les performances sont identiques par rapport à la première version.
Nos modifications dans le code n’ont pas grevé les performances du jeu sans pour autant les
améliorer puisque nous ne sommes pas tellement intervenu sur des parties critiques pour cellesci. Une approche d’optimisation pourrait être de réécrire certains algorithmes de mise à jour,
mais les performances du jeu étant satisfaisantes (le jeu étant jouable même sur une machine
virtuelle, de type Virtualbox), une telle chose est peu utile.
3.3.4
La couverture du code
Tester la couverture du code avec gcov sur un programme comme Dungeon Digger pose
quelques problèmes, car il est préférable de compiler le code à tester sans optimisation. Malheureusement, le jeu est injouable sans les optimisations de compilation apportées par gcc. Hors,
36
Chapitre 3 : Réusinage et développement
Fig. 3.1 – Exemple de test réalisé avec gcov
il faut pouvoir tester le jeu au maximum de ses capacités pour obtenir un résultat de test de
couverture aussi précis que possible. Ceci explique donc le fait que les résultats obtenus ne veulent pas dire grand chose, et n’indique pas la nécessité à faire des optimisations sur les points
où le programme n’exécute pas certaines lignes de code.
Les conditions optimales pour obtenir un résultat de test précis seraient de lancer le jeu dans
un cadre multijoueurs.
3.3.5
Test du serveur
Un dernier test nous paraissant important concernant les performances générales de l’application, était la charge que le serveur peut accepter. Nous avons défini des conditions de test
précises : sur deux postes avec un système de type Ubuntu, nous avons lancé sur un premier
poste le serveur (./digger-server) puis 3 clients (./digger), puis 5 clients sur un second poste.
Nous avons ensuite fait évoluer les jeux de chaque client.
Résultat : aucun problème n’a affecté le serveur. Il peut tout à fait accepter 8 clients, voire
même plus, ce qui reste à tester. L’ensemble du jeu est géré de manière fluide, aucune saccade
ou retard n’était à déclarer sur les clients. Le seul “problème” vient du peu de fluidité constatée
dans certaines phases de jeu, mais ceci est tout à fait normal, vu le nombre d’unités affichées à
l’écran, à gérer par la carte graphique de la machine. Le serveur a tourné pendant un laps de
temps de 1 heure, jusqu’au moment où nous avons arrêté nos tests.
Les seuls crashs constatés ont eu lieu sur les clients. En effet, lorsque deux ennemis se rencontrent sur la map, ils vont chercher à convertir les cases du camp adverse. Pendant un certain
moment, les cases vont donc changer de couleur sans arrêt, jusqu’à provoquer le crash des clients
concernés, quand à un moment, deux buddies adverses cherchent à convertir la même case au
même moment. On voit ici les faiblesses de l’implémentation du gameplay, et du tasker (qui sont
issues de la version originale du jeu, aucune modification n’ayant eue lieu, à part d’un point de
vue refactoring). Ces deux éléments doivent être améliorés dans une implémentation futur pour
permettre une vraie expérience de jeu.
3.3 Tests
37
Fig. 3.2 – Le serveur chargé avec 8 clients
Fig. 3.3 – 2 ennemis se rencontrent... Crash assuré
Bibliographie
[1] Michael Dawson. Beginning C++ Through Game Programming. Delmar Learning, second
edition, 2007.
[2] Mark DeLoura, Steve Rabin, et al. Game Programming Gems Collection. Charles River
Media, 2000.
[3] Martin Fowler. Refactoring : Improving the Design of Existing Code. Pearson Education,
November 2002.
[4] Robert C. Martin. Clean Code : A Handbook of Agile Software Craftsmanship. Prentice Hall,
August 2008.
[5] Mike McShaffry. Game Coding Complete. Charles River Media, third edition, March 2009.
[6] Ron Penton. Data Structures for Game Programmers. Muska & Lipman/Premier-Trade,
November 2002.
[7] Rudy Rucker. Software Engineering and Computer Games. Addison-Wesley Educational,
2002.
[8] Craig Silverstein, Gregory Eitzmann, Mark Mentovai, Benjy Weinberger, and Tashana Landray. Google C++ Style Guide. Google, 2008.
Commentaires
Le sujet de notre Projet de Programmation ayant été d’améliorer le code (puis le gameplay)
d’un jeu, il y a deux sortes d’ouvrages auquel il était intéressant pour nous de se référer, concernant respectivement :
– Le réusinage et l’organisation d’un code [3, 4]
– le développement d’un jeu vidéo et notamment la programmation [1, 2, 5, 6, 7, 8].
Bien entendu, certains ouvrages regroupent plusieurs thématiques. Les ouvrages traitant du
développement des jeux permettent de s’inspirer de modèles de codes déjà éprouvés et de recentrer les concepts sur la structuration du code au domaine assez particulier du jeu.
Il est à noter que certains volumes ne nous seront utiles qu’en partie. Par exemple, R. Rucker[7]
considère le génie logiciel dans le contexte du jeu vidéo, puis s’intéresse aux environnements de
développement propres à Windows, ce qui n’est pas intéressant dans notre cas.
38
Annexe
Extrait de code
buddy.h
Cet extrait correspond au fichier buddy.h, le header consacré à la gestion des buddies (”bonhommes”, c’est à dire aux unités). Il s’agit d’un des premiers fichiers réusinés, et un de ceux qui
ont pris le plus de temps, car comme il hérite de nombreuses autres classes, toucher aux fonctions
qu’elles partageaient entre elles était une tâche sensible. Certaines fonctions sont ainsi restées
avec des noms non conformes au standard, et on peut les trouver à la fin, sous le commentaire
Those functions are very delicate.
Dans le code affiché, il y a certaines ellipses : le commentaire entier qui sert d’en-tête au fichier
n’est pas entier et les accesseurs ne sont pas montrés.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/∗
∗ This s o u r c e f i l e i s p a r t o f Dungeon D i g g e r
∗ W e b s it e : h t t p : / / s o u r c e f o r g e . n e t / p r o j e c t s / d u n g e o n d i g g e r /
∗
∗ C o p y r i g h t (C) 2006 Dungeon D i g g e r Team
∗
∗ [...]
∗/
/∗
∗ This f i l e d e s c r i b e t h e Buddy and BuddyKind c l a s s e s . A ” buddy ”
∗ i s a movable NPC, s i m i l a r t o s o l d i e r s and peons o f o t h e r games .
∗ BuddyKinds a r e RPG ” c h a r a c t e r c l a s s e s ” , l i k e Barbarian , Healer ,
∗ S o r c e r e r , and so on . They were c a l l e d B u d d y C l a s s e s b e f o r e , b u t i t
∗ was a l w a y s a c h o r e t o e x p l a i n t h e d i f f e r e n c e w i t h C / C++ c l a s s e s .
∗/
#i f n d e f GAME BUDDY H
#define GAME BUDDY H
#include ”buddy . h”
#include <math . h>
#include
#include
#include
#include
#include
” c e l l . h”
” e n t i t y . h”
”hpp . h”
” u s e r i d . h”
” r a c e . h”
enum BuddyStatus {
39
40
BIBLIOGRAPHIE
32
BUDDY OK,
33
BUDDY KO,
34
BUDDY DEAD,
35
BUDDY REMOVE
36 } ;
37
38 c l a s s TaskerEnt ;
39
40 c l a s s BuddyKind : public V o l a t i l e K i n d {
41
public :
42
BuddyKind ( EnumTId t , const s t r i n g &n ) ;
43
v i r t u a l ˜BuddyKind ( ) {}
44
Buddy ∗ NewInstance ( const U s e r I d &own , double x = 0 . , double y = 0 . ,
45
Lmap ∗lm = NULL ) ;
46
double AttackPower ( const Buddy &) const ;
47
double D e f e n s e R a t i n g ( const Buddy &) const ;
48
[...]
49
private :
50
EnumTId b u d d y t y p e ;
51
RaceType r a c e t y p e ;
// Unimplemented , b u t c o u l d be u s e f u l l a t e r
52
double r o t a t i o n s p e e d ; // Unimplemented , b u t c o u l d be u s e f u l l a t e r
53
double b a s e a t t a c k ;
54
double b a s e d e f e n s e ;
55
double b a s e s p e e d ;
56
double f i e l d o f v i s i o n ;
57 } ;
58
59 i n l i n e BuddyKind ∗GetBuddyKind ( i n t i ) {
60
return dynamic cast<BuddyKind∗> ( g e t e n t c l a s s ( i ) ) ;
61 }
62
63 extern void InitBuddyKinds ( ) ;
64
65 c l a s s Buddy : public V o l a t i l e {
66
public :
67
Task t a s k ; // t h e t a s k ( w i t h t h e t a r g e t ) a s s i g n e d t o t h e buddy
68
69 #i f d e f CLIENTSIDE
70
TaskerEnt ∗ t a s k e r e n t ;
71 #endif
72
73
Buddy ( EnumTId t , const U s e r I d &own , double x = 0 . , double y = 0 . ,
74
double o = 0 . , const s t r i n g &s t r = ” ” ) ;
75
v i r t u a l ˜Buddy ( ) ;
76
const BuddyKind∗ C( ) const ;
77
const BuddyKind∗ GetKind ( ) const ;
78
double Attack ( ) const ;
79
double Defend ( ) const ;
80
double V i s i o n ( ) const ;
81
bool I s I n t e r a c t i n g ( ) const ;
82
bool IsOK ( ) const ;
83
bool I s W a i t i n g ( ) const ;
84
void I n c r e a s e E x p e r i e n c e ( double a , double m) ;
85
void D e c r e a s e L i f e ( double a ) ;
86
double Speed ( const C e l l &c ) const ;
87
s t a t i c double CBSpeed ( const C e l l &c ) ;
88
[...]
89
R e s o u r c e s r e s o u r c e s ( ) const ;
90
R e s o u r c e s m a x r e s o u r c e s ( ) const ;
91
i n t c a r r y i n g e n t i t y ( ) const ;
92
[...]
BIBLIOGRAPHIE
93
void FreeTaskerEnt ( ) ;
94
v i r t u a l void Move ( double s ) ;
95
v i r t u a l void MoveTo ( double x , double y ) ;
96
v i r t u a l bool ToRemove ( ) const ;
97
v i r t u a l Message S e r i a l i z e ( ) const ;
98
v i r t u a l Message S e r i a l i z e T o H i d e ( ) const ;
99
v i r t u a l Message S e r i a l i z e T o D e l ( ) const ;
100
void ProcessTime ( double d e l a y ) ;
101
void PreemptProcessTime ( double d e l a y ) ;
102
v i r t u a l I n t e r a c t a b l e ∗ Clone ( ) const ;
103
v i r t u a l void Update ( I n t e r a c t a b l e ∗ o ) ;
104
v i r t u a l void S h i f t ( i n t i , i n t j ) ;
105
// Those f u n c t i o n s a r e v e r y d e l i c a t e
106
bool c o l l i s i o n ( const C e l l &c ) const ;
107
void d e l t a s k ( ) ;
108
s t a t i c Buddy∗ u n s e r i a l i z e ( const Message &s ) ;
109
private :
110
BuddyStatus s t a t u s ;
111
double r e c ; // 0 . . 1
112
double t x , t y ;
113
bool moving ;
114
// S t a t s
115
double l i f e ; // 0 . . 1
<0.1: KO <=0.: dead
116
double s l e e p ; // 0 . . 1
117
double e x p e r i e n c e ; // 1 . . 1 0
118
double f o o d ; // 0 . . 1
<0.5: hungry −− Unused f o r now
119
// R e s o u r c e s c a r r y i n g
120
Resources r e s o u r c e s ;
121
Resources max resources ;
122
int c a r r y i n g e n t i t y ;
123 } ;
124
125 #endif // GAME BUDDY H
41
Téléchargement