Vous êtes sur la page 1sur 366

Développement

XNA
pour la Xbox et le PC
Premiers pas en développement de jeu vidéo

Léonard Labat
Développement
XNA
pour la Xbox et le PC
Chez le même éditeur Freemind – Boostez votre efficacité. X. Delengaigne, P. Mongin.
N°12448, 2009, 272 pages.
Dans la thématique du jeu vidéo
Spip 2 – Premiers pas pour créer son site avec Spip 2.0.3.
RPG Maker. Créez votre gameplay et déployez votre jeu de rôle. A.-L. Quatravaux, D. Quatravaux. – N°12502, 2009, 300
S. Ronce. – N°12562, à paraître. pages.

Équideow. Le guide du bon éleveur. Perline et L. Noisette. – Réussir son site web avec XHTML et CSS. M. Nebra. –
N°12521, à paraître. N°12307, 2e édition, 2008, 306 pages.
Réussir un site web d’association… avec des outils libres !
Dans la même collection
A.-L. Quatravaux et D. Quatravaux. – N°12000, 2e édition,
ActionScript 3. Programmation séquentielle et orientée objet. 2007, 372 pages.
D. Tardivau. – N°12552, 2e édition, 2009,448 pages. Réussir son site e-commerce avec osCommerce. D. Mercer. –
PHP/MySQL avec Dreamweaver CS4. Les clés pour réussir son N°11932, 2007, 446 pages.
site marchand. J.-M. Defrance. – N°12551, 2009, 548 pages. Open ERP – Pour une gestion d’entreprise efficace et intégrée.
Sécurité PHP 5 et MySQL. D. Seguy et P. Gamache. – F. Pinckaers, G. Gardiner. – N°12261, 2008, 276 pages.
N°12554, 2009, 284 pages. PGP/GPG – Assurer la confidentialité de ses mails et fichiers.
Sécurité informatique. Principes et méthode à l’usage des DSI, M. Lucas, ad. par D. Garance , contrib. J.-M. Thomas. –
RSSI et administrateurs. L. Bloch, C. Wolfhugel. – N°12525, N°12001, 2006, 248 pages.
2009, 292 pages. Mozilla Thunderbird – Le mail sûr et sans spam. D. Garance,
Programmation OpenOffice.org 3. Macros, OOoBASIC et API. A.-L. et D. Quatravaux. – N°11609, 2005, 300 pages avec
B. Marcelly et L. Godard. – N°12522, 2009, 920 pages. CD-Rom.

Dreamweaver CS4 Styles CSS. Composants Spry-XM, Firefox. Retrouvez votre efficacité sur le Web ! T. Trubacz,
comportements JavaScrip, comportements serveur PHP-MySQL. préface de T. Nitot. – N°11604, 2005, 250 pages.
T. Audoux et J.-M. Defrance. – N°12462, 2009, 620 pages. Hackez votre Eee PC – L’ultraportable efficace. C. Guelff. –
Programmation Python. Conception et optimisation. T. Ziadé. – N°12437, 2009, 306 pages.
N°12483, 2e édition, 2009, 586 pages. Monter son serveur de mails Postfix sous Linux. M. Bäck et
CSS2. Pratique. du design web. R. Goetter. – N°12461, al., adapté par P. Tonnerre. – N°11931, 2006, 360 pages.
3e édition, 2009, 318 pages. Ergonomie web – Pour des sites web efficaces.
Programmation Flex 3. Applications Internet riches avec Flash A. Boucher. – N°12479, 2e édition 2009, 440 pages.
ActionScript 3, MXML et Flex Builder. A. Vannieuwenhuyze. – Joomla et VirtueMart – Réussir sa boutique en ligne.
N°12387, 2008, 430 pages.
V. Isaksen, avec la contribution de T. Tardif. – N°12381, 2008,
WPF par la pratique. T. Lebrun. – N°12422, 2008, 318 pages. 306 pages.

PHP 5 avancé. E. Daspet et P. Pierre de Geyer. – N°12369, La 3D libre avec Blender. O. Saraja. – N°12385, 3e édition,
5e édition, 2008, 884 pages. 2008, 456 pages avec DVD-Rom.

Bien développer pour le Web 2.0. Bonnes pratiques Ajax - Dessiner ses plans avec QCad – Le DAO pour tous. A. Pascual
Prototype, Script.aculo.us, accessibilité, JavaScript, DOM, – N°12397, 2009, 278 pages.
XHTML/CSS. C. Porteneuve. – N°12391, 2e édition, 2008, 674
Inkscape efficace. C. Gémy – N°12425, 2009, 280 pages.
pages.
Ubuntu efficace. L. Dricot. – N°12362, 3e édition, à paraître
Dans la collection « Accès Libre » 2009.
Linux aux petits oignons. K. Novak. – N°12424, 2009, 546 Gimp 2.6 – Débuter en retouche photo et graphisme libre.
pages. D. Robert. – N°12480, 4e édition, 2009, 350 pages.
Inkscape. Premiers pas en dessin vectoriel. N. Dufour, collab. Gimp 2.4 efficace – Dessin et retouche photo. C. Gémy. –
E. de Castro Guerra. – N°12444, 2009, 376 pages. N°12152, 2008, 402 pages avec CD-Rom.
MediaWiki efficace. D. Barrett. – N°12466, 2009, 372 pages. Dotclear 2 – Créer et administrer son blog. A. Caillau. –
Économie du logiciel libre. F. Elie. – N°12463, 2009, 195 pages. N°12407, 2008, 242 pages.
Développement
XNA
pour la Xbox et le PC
Premiers pas en développement de jeu vidéo

Léonard Labat
ÉDITIONS EYROLLES
61, bd Saint-Germain
75240 Paris Cedex 05
www.editions-eyrolles.com

Le code de la propriété intellectuelle du 1er juillet 1992 interdit en effet expressément la photocopie à
usage collectif sans autorisation des ayants droit. Or, cette pratique s’est généralisée notamment dans les
établissements d’enseignement, provoquant une baisse brutale des achats de livres, au point que la possibilité
même pour les auteurs de créer des œuvres nouvelles et de les faire éditer correctement est aujourd’hui
menacée.
En application de la loi du 11 mars 1957, il est interdit de reproduire intégralement ou partiellement le
présent ouvrage, sur quelque support que ce soit, sans autorisation de l’éditeur ou du Centre Français d’Exploitation du
Droit de Copie, 20, rue des Grands-Augustins, 75006 Paris.
© Groupe Eyrolles, 2009, ISBN : 978-2-212-12458-3
=Labat FM.book Page V Vendredi, 19. juin 2009 4:01 16

Avant-propos

Si vous lisez ce livre, c’est que votre objectif est sûrement de créer un jeu vidéo, c’est-à-
dire d’ordonner à l’ordinateur ou à la console d’effectuer un certains nombres de tâches.

La programmation de jeu vidéo


Lors d’une utilisation quotidienne d’un ordinateur ou de votre console, vous n’avez nul
besoin de programmer. Si vous devez faire une recherche sur l’Internet ou que vous
voulez jouer à un jeu, vous vous contenterez d’utiliser un programme écrit par quelqu’un
d’autre ; et ceci est tout à fait normal, nul besoin d’être plombier pour prendre un bain !

Définition
Un programme informatique a pour but d’indiquer à un ordinateur la liste des étapes nécessaires à la réali-
sation d’une tâche. La programmation est le nom donné au processus de création d’un programme.

Pour certains, la programmation constitue une véritable passion, pour d’autres, c’est un
moyen pratique de donner une solution à un problème… Dans tous les cas, force est de
constater que la programmation devient un hobby et pénètre dans l’univers du grand
public. Pierre angulaire de la science informatique, c’est une activé fascinante qui attire
et motive de nombreux étudiants vers de réelles opportunités de travail, qu’il s’agisse de
l’univers du jeu ou non. Toutefois, elle n’en reste pas moins un domaine complexe et de
surcroît en constante évolution.
Mais la passion n’est pas le seul ingrédient requis pour réussir ses programmes… On ne
s’improvise pas spécialiste en informatique ! En effet, la création d’un jeu n’est pas
seulement affaire de programmation : il faut aller au-delà et s’attaquer à la partie graphique,
audio et bien évidemment au gameplay.
Les concepts qui seront abordés dans ce livre vous donneront de solides bases, mais ne
soyez pas déçu si vos premiers jeux n’égalent pas les réalisations sophistiquées auxquelles
vous êtes habitué. C’est une expérience incroyable que de voir une de ses créations
prendre forme, et même si le challenge est parfois difficile, la récompense est toujours
très gratifiante.
=Labat FM.book Page VI Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la Xbox et le PC


VI

Code intelligible, code machine


Un ordinateur ne comprend que des instructions très simples :
1. Récupérer le contenu d’un emplacement mémoire.
2. Lui appliquer une opération mathématique basique.
3. Déplacer le résultat vers un autre emplacement mémoire.
• En plus de diviser à l’extrême chaque tâche, pour être compris directement par l’ordi-
nateur, vous devez lui parler en binaire, c’est-à-dire en une succession de 0 et 1. Imaginez
donc la complexité du code machine qui se cache derrière le démineur de Microsoft…
• Ce type de code n’étant pas du tout intelligible par un humain, il a donc fallu créer des
langages possédant une syntaxe plus proche de notre langue ainsi que les outils nécessaires
à la traduction du code écrit dans ces langages vers le code machine correspondant.
Ces derniers sont généralement appelés compilateurs.
• On distingue plusieurs types de langages : ceux dits de bas niveau et ceux de haut niveau.
Plus un langage est de bas niveau, plus il se rapproche de la machine, c’est-à-dire que
sa syntaxe est moins innée, que la gestion de la mémoire est plus difficile, etc. Prenons
deux exemples. L’assembleur étant un langage de bas niveau, il faut traiter directement
avec les registres du processeur, et il implique une bonne connaissance de l’architecture
système. À l’inverse, le Visual Basic est un langage plus abordable qui n’est pas soumis
aux mêmes contraintes que celles que nous venons de citer.
• Il faut surtout garder en tête qu’un langage qui pourrait être classé de plus haut niveau
n’est pas forcément plus facile à maîtriser qu’un autre. Tout dépend du programmeur,
bien sûr, mais aussi du besoin : à cause de sa simplicité, le Visual Basic n’offre pas les
mêmes possibilités d’optimisation que le C, par contre, il s’avère très pratique pour
développer rapidement une application.

Les algorithmes
Un algorithme est l’énoncé d’une suite d’opérations constituant une solution à un problème
donné. On peut présenter toutes les actions de notre quotidien sous la forme algorithmique.
Par exemple, pour la cuisson des pâtes :
1. Saler l’eau.
2. Porter à ébullition.
3. Plonger les pâtes.
4. Mélanger pour éviter qu’elles ne collent au fond.
5. Égoutter.
6. Rincer.
Grâce à cet algorithme, vous pouvez aisément expliquer à quelqu’un la façon de cuire des
pâtes, si besoin est.
=Labat FM.book Page VII Vendredi, 19. juin 2009 4:01 16

Avant-propos
VII

Le langage algorithmique est un compromis entre notre langage courant et un langage de


programmation. Ainsi, la compréhension d’une fonction d’un programme est plus aisée
qu’en se plongeant directement dans le code.

XNA et son environnement


Il existe une multitude de langage de programmation et de bibliothèques qui peuvent être
utilisés pour programmer un jeu vidéo. Comment faire le bon choix ?

Pourquoi choisir XNA ?


L’un des principaux critères qui peut motiver votre choix est la plate-forme cible. En
effet, vous n’utiliserez pas forcément les mêmes outils pour créer un jeu pour Xbox 360
ou téléphone mobile. D’une manière générale, pour développer un jeu pour console, vous
devrez utiliser un kit de développement adapté : la PSP possède son SDK utilisable
en C++, celui de la Nintendo DS repose quant à lui sur le C.
Du côté des PC, vous pouvez programmer un jeu vidéo dans un peu près n’importe quel
langage. En ce qui concerne la partie graphique du jeu, deux solutions s’offrent à vous : la
première consiste à utiliser des bibliothèques de très bas niveau telles que DirectX, OpenGL
ou encore SDL. La seconde possibilité consiste à utiliser un moteur graphique comme
OGRE ou Allegro. Elles est particulièrement intéressante car elle permet de gagner beaucoup
de temps.
XNA est une bibliothèque de bas niveau basée sur le framework Compact .Net dans son
implémentation pour Xbox 360 (ou le lecteur multimédia Zune de Microsoft) et sur le
framework .Net dans son implémentation pour PC.

Comprendre le framework .NET


• Le framework .NET (prononcez « dotNet »), est un composant Windows apparu dans
sa version 1.0 en 2002. Depuis, Microsoft a sorti régulièrement de nouvelles versions.
Avec le système d’exploitation Windows XP, ce composant était facultatif. Cependant
la version 3.0 du framework, .NET est directement intégré à Windows Vista.

En détail
Voici récapitulées les années de sortie des précédentes versions de notre framework :
1.1 en 2003 ;
2.0 en 2005 ;
3.0 en 2006 ;
3.5 en 2007.

• Il dispose de deux atouts majeurs pour simplifier le développement d’applications web


ou Windows : le CLR (Common Language Runtime) et les bibliothèques de classes.
=Labat FM.book Page VIII Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la Xbox et le PC


VIII

• Le CLR est une machine virtuelle (bien que Microsoft préfère utiliser le terme runtime)
utilisée pour exécuter une application .NET. Il possède, entre autres, un composant appelé
JIT (Just In Time, c’est-à-dire juste à temps), qui compile du code MSIL (Microsoft
Intermediate Language) vers du code compréhensible par la machine. Ainsi, tout langage
disposant d’un compilateur qui produit du code MSIL (les spécifications techniques
sont disponibles à cette adresse : http://www.ecma-international.org/publications/
standards/Ecma-335.htm/) est exécutable par le CLR et bénéficie des possibilités offertes
par la plate-forme. Il est donc possible de choisir un langage parmi un grand nombre
(C#, C++, VB.NET, J#, etc.), le choix ne dépendant plus forcément des performances
mais plutôt d’une affaire de goût. Le CLR comporte également une multitude d’autres
technologies dont vous ne saisiriez peut-être pas l’intérêt pour le moment, mais que
nous aborderons plus tard dans cet ouvrage.

MSIL
Langage ressemblant à de l’assembleur, MSIL ne comporte aucune instruction propre à un système
d’exploitation ou à du matériel.

Le framework .NET met également à la disposition du programmeur plus de 2 000 classes


utilitaires, qui lui permettent de gagner un temps précieux lors du développement. Ainsi,
manipulation de chaînes de caractères, communication réseau, accès aux données sont
choses faciles à réaliser. À chaque nouvelle version du framework, la bibliothèque de classes
s’étoffe davantage et les fonctionnalités disponibles sont de plus en plus performantes.

XNA : faciliter le développement de jeu vidéo


Le framework XNA (XNA’s Not Acronymed) est constitué de plusieurs bibliothèques .NET
et permet un développement multi-plate-forme : les classes fournies par XNA permettent
au programmeur de développer un jeu pour Windows puis de le porter très facilement
pour qu’il soit utilisable sur Xbox 360 ou sur le lecteur multimédia Zune.
L’un des buts de XNA est de simplifier au maximum le développement de jeu vidéo. Par
exemple, si vous avez déjà eu une expérience dans le développement avec l’api DirectX
ou OpenGL, vous savez certainement qu’écrire l’initialisation de votre programme vous
prendrait un certain temps alors qu’avec XNA tout est automatique. C’est précisément là
que réside tout l’intérêt du framework : avec XNA, il vous suffit seulement d’écrire quelques
lignes de code très facilement compréhensibles pour créer un jeu complet.

Bon à savoir
Soulignons également que le framework XNA est livré avec ce que l’on appelle des Starter Kit. Ces petits
projets de jeu vidéo montrent les possibilités offertes ainsi que le niveau d’accessibilité du développement.
=Labat FM.book Page IX Vendredi, 19. juin 2009 4:01 16

Avant-propos
IX

Officiellement, XNA ne peut être utilisé qu’avec le langage de programmation C#. En


pratique, vous pouvez également réaliser un jeu avec XNA en VB.NET, mais vous ne
pourrez pas utiliser tous les composants offerts par le framework.

Version
XNA 3.0 est disponible depuis le 30 octobre 2008, c’est sur cette version que ce livre se focalise.

C#, langage de programmation de XNA


Langage de programmation orienté objet à typage fort, C# (prononcez « C-Sharp ») a fait
son apparition avec la plate-forme .NET. Il est très proche à la fois du Java et du C++. Ses
détracteurs le qualifient souvent de copie propriétaire de Java.

Java
Très répandu dans le monde du logiciel libre, ce langage s’exécute lui aussi sur une machine virtuelle. À
l’heure actuelle et selon des sondages qui paraissent régulièrement sur l’Internet, il s’agit du langage le
plus populaire parmi les développeurs.

Tout comme le framework .NET dont il est indissociable, le langage C# est régulièrement
mis à jour et se voit ajouter des améliorations syntaxiques ou de conception.

Choisir son environnement de développement intégré


Pour utiliser XNA ou, d’une manière plus générale, programmer dans un langage compa-
tible .Net, vous aurez besoin d’un EDI (Environnement de Développement Intégré).
Microsoft en propose toute une gamme comprenant :
• Visual Studio Express.
• Visual Studio Standard.
• Visual Studio Professional.
• Visual Studio Team System.
Chaque version vise un public différent, les versions Express (il en existe une pour le
langage C#, une pour le C++, une pour le VB et une pour le développement web) sont
gratuites et s’adressent au développeur amateur tandis que la version Team System est
orientée pour le développement professionnel en équipe.
XNA 3.0 est compatible avec les versions de Visual Studio 2008. Dans ce livre, nous
utiliserons la version Microsoft Visual C# Express 2008.
Vous connaissez maintenant tous les outils nécessaires pour commencer, alors bonne
lecture et bienvenue dans le monde du C# et de XNA !
=Labat FM.book Page X Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la Xbox et le PC


X

À qui s’adresse le livre ?


Ce livre s’adresse à tous ceux qui désirent créer des jeux pour PC, pour Xbox 360 ou pour
le Zune sans avoir d’expérience préalable dans ce domaine ou même dans celui plus vaste
de la programmation. En effet, nous y présentons les notions de bases du C# nécessaires
à la compréhension de XNA.
Ainsi, ce livre vous sera utile si, étudiant en programmation, vous souhaitez découvrir
l’univers du développement de jeux vidéo ; si vous travaillez au sein d’un studio indé-
pendant ou en tant que freelance et que vous souhaitez vous former aux spécificités de
développement pour Xbox ; ou si, tout simplement, vous êtes curieux de vous initier au
développement de jeu et que vous avez choisi XNA.
Cependant, nous vous conseillons tout de même de vous munir d’un ouvrage sur le
langage de programmation C# : ce livre ne constitue pas un document de référence sur ce
langage, nous ne verrons ici que ce qui sera utile à la compréhension du framework
XNA, et certaines facettes du langage seront mieux détaillées dans un ouvrage spécialisé.

Structure de l’ouvrage
Le chapitre 1 présente les notions de base du langage de programmation C#, qui vous
seront utiles dès le chapitre 2 à la création d’une première application avec XNA.
Nous attaquerons les choses sérieuses dans le chapitre 3 en apprenant à afficher de
premières images à l’écran puis, dans le chapitre 4, nous apprendrons à récupérer les
entrées utilisateur sur le clavier, la souris ou la manette de la Xbox 360. Ces notions
seront mises en pratique avec la création d’un clone de Pong dans le chapitre 5. Le
chapitre 6 poussera plus loin les fonctions d’affichage d’images dans XNA.
Dans le chapitre 7, vous étofferez votre jeu en lui ajoutant un environnement sonore qu’il
s’agisse des sons ou de morceaux de musique. Puis, dans le chapitre 8, vous découvrirez
les techniques de lecture ou d’écriture de fichiers qui entrent en jeu dans les fonctionnalités
de sauvegarde.
Dans le chapitre 9, vous vous écarterez un peu du monde de XNA pour rejoindre celui
des sciences cognitives et plus particulièrement l’implémentation d’un algorithme de
recherche de chemin. Le chapitre 10 abordera également un domaine qui n’est pas propre
à XNA : la gestion de la physique. Nous verrons donc comment implémenter un moteur
physique.
Dans le chapitre 11, le dernier à utiliser des exemples en deux dimensions, vous décou-
vrirez comment créer un jeu multijoueur avec XNA, qu’il s’agisse d’un jeu sur écran
splitté ou en réseau.
Le chapitre 12 propose une introduction à la programmation de jeux en 3D avec XNA.
Pour terminer, dans le chapitre 13, vous apprendrez à réaliser des effets en HLSL.
Si vous n’avez jamais utilisé l’IDE Visual Studio, ou si vous souhaitez compléter vos
connaissances, l’annexe A est consacrée à sa prise en main. L’annexe B vous donne des
=Labat FM.book Page XI Vendredi, 19. juin 2009 4:01 16

Avant-propos
XI

pistes pour que vous puissiez pousser votre exploration de XNA au-delà de ce livre. Elle
présente donc différentes sources d’informations disponibles sur le Web, ainsi que des
méthodes de génération de documentation pour vos projets.

Remerciements
Je tiens tout d’abord à remercier Aurélie qui partage ma vie depuis un moment déjà et qui
sait toujours faire preuve de compréhension lorsque je passe des heures scotché à mon
ordinateur à coder encore et encore.
Merci également à mes parents qui ont tout mis en œuvre pour que j’accomplisse mes
rêves et sans qui je n’aurais sûrement jamais écrit ce livre.
Enfin je remercie les éditions Eyrolles, et tout particulièrement Sandrine et Muriel qui
m’ont accompagné tout au long de la rédaction de cet ouvrage.

Léonard Labat
Developper.xna@gmail.com
=Labat FM.book Page XII Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page XIII Vendredi, 19. juin 2009 4:01 16

Table des matières

Avant-propos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . V
La programmation de jeu vidéo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . V
Code intelligible, code machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VI
Les algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VI
XNA et son environnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VII
Pourquoi choisir XNA ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VII
Comprendre le framework .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VII
XNA : faciliter le développement de jeu vidéo . . . . . . . . . . . . . . . . . . . . . . . . VIII
C#, langage de programmation de XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IX
Choisir son environnement de développement intégré . . . . . . . . . . . . . . . . . . IX
À qui s’adresse le livre ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . X
Structure de l’ouvrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . X
Remerciements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XI

CHAPITRE 1

Débuter en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Créez votre premier programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Les types de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Organisation de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Les variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Opérations de base sur les variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Les instructions de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Commenter son code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Les conditions : diversifier le cycle de vie des jeux . . . . . . . . . . . . . . . . . . . . 11
=Labat FM.book Page XIV Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


XIV

Les fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Différencier fonction et procédure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Écrire une première procédure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Écrire une première fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Les classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Comprendre les classes et les objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Utiliser un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Qu’est ce qu’un espace de noms ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Créer une classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

CHAPITRE 2
Prise en main de XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Télécharger l’EDI et XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Partir d’un starter kit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Partager ses projets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
L’architecture d’un projet XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Structure du framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Structure du code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Créer un projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
S’outiller pour développer sur Xbox 360 . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

CHAPITRE 3
Afficher et animer
des images : les sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Les sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Qu’est-ce qu’un sprite ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Afficher un sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Afficher plusieurs sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Un sprite en mouvement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Une classe pour gérer vos sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Créer une classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Utiliser la classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Classe héritée de Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
=Labat FM.book Page XV Vendredi, 19. juin 2009 4:01 16

Table des matières


XV

Un gestionnaire d’images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Les boucles en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Les collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Écriture du gestionnaire d’images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Mesure des performances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

CHAPITRE 4

Interactions avec le joueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67


Utiliser les périphériques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Le clavier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
La souris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
La manette de la Xbox 360 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Utilisation de périphériques spécialisés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Les services avec XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Les interfaces en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Comment utiliser les services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Les méthodes génériques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Toujours plus d’interactions grâce à la GUI . . . . . . . . . . . . . . . . . . . . . . . . 80
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

CHAPITRE 5

Cas pratique : programmer un Pong . . . . . . . . . . . . . . . . . . . . . . . . . . . 83


Avant de se lancer dans l’écriture du code . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Définir le principe du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Formaliser en pseudo-code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Développement du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Création du projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
L’arrière-plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Les raquettes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
La balle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Améliorer l’intérêt du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
=Labat FM.book Page XVI Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


XVI

CHAPITRE 6
Enrichir les sprites : textures, défilement, transformation,
animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Préparation de votre environnement de travail . . . . . . . . . . . . . . . . . . . . . . 97
Texturer un objet Rectangle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Modifier la classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Faire défiler le décor : le scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Créer des animations avec les sprites sheets . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Varier la teinte des textures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Opérer des transformations sur un sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
Afficher du texte avec Spritefont . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Afficher le nombre de FPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

CHAPITRE 7
La sonorisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Travailler avec XACT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Créer un projet sonore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Lire les fichiers créés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Lire les fichiers en streaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
Compression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Ajouter un effet de réverbération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Le son avec la nouvelle API SoundEffect . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Lire un son . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Lire un morceau de musique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Pour un bon design sonore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

CHAPITRE 8
Exceptions et gestion des fichiers : sauvegarder
et charger un niveau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Le stockage des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Les espaces de stockage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Sérialisation et désérialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Les exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
=Labat FM.book Page XVII Vendredi, 19. juin 2009 4:01 16

Table des matières


XVII

Les Gamer Services : interagir avec l’environnement . . . . . . . . . . . . . . . . 152


Dossier de l’utilisateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Les méthodes asynchrones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
La GamerCard : la carte d’identité du joueur . . . . . . . . . . . . . . . . . . . . . . . . . 158
Version démo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
La sauvegarde en pratique : réalisation d’un éditeur de cartes . . . . . . . . 162
Identifier les besoins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Chemin du dossier de jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Gérer les dossiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
Manipuler les fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Écrire dans un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Lire un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Sérialiser des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Désérialiser des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Les Content Importers, une solution compatible avec la Xbox 360 . . . . 181
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184

CHAPITRE 9
Pathfinding : programmer les déplacements des personnages 185
Les enjeux de l’intelligence artificielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Comprendre le pathfinding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
L’algorithme A* : compromis entre performance et pertinence . . . . . . . 187
Principe de l’algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Implanter l’algorithme dans un jeu de type STR . . . . . . . . . . . . . . . . . . . . . . 190
Cas pratique : implémenter le déplacement d’un personnage
sur une carte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
Préparation : identifier et traduire les actions du joueur . . . . . . . . . . . . . . . . . 200
Créer le personnage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Implémenter l’algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206

CHAPITRE 10
Collisions et physique : créer un simulateur
de vaisseau spatial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Comment détecter les collisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Créer les bases du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Établir une zone de collision autour des astéroïdes . . . . . . . . . . . . . . . . . . . . . 213
=Labat FM.book Page XVIII Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


XVIII

Simuler un environnement spatial : la gestion de la physique . . . . . . . . . 217


Choisir un moteur physique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
Télécharger et installer FarseerPhysics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Prise en main du moteur physique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Les collisions avec FarseerPhysics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230

CHAPITRE 11

Le mode multijoueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231


Jouer à plusieurs sur le même écran . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Du mode solo au multijoueur : la gestion des caméras . . . . . . . . . . . . . . . 232
Créer un jeu solo avec effet de scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Adapter les caméras au multijoueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Personnaliser les différentes vues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
Le multijoueur en réseau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
S’appuyer sur la plate-forme Live . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
Implémenter les fonctionnalités de jeu en réseau . . . . . . . . . . . . . . . . . . . . . . 249
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257

CHAPITRE 12

Les bases de la programmation 3D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259


L’indispensable théorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Le système de coordonnées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Construire des primitives à partir de vertices . . . . . . . . . . . . . . . . . . . . . . . . . 260
Les vecteurs dans XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Les matrices et les transformations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
Gérer les effets sous XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
Comprendre la projection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
Dessiner des formes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
La caméra et la matrice de projection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
La matrice de vue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Des vertices à la forme à dessiner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Déplacer la caméra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Appliquer une couleur à un vertex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
=Labat FM.book Page XIX Vendredi, 19. juin 2009 4:01 16

Table des matières


XIX

Plaquer une texture sur un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281


Texturer une face d’un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Texturer un objet entier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
Déplacer un objet avec les transformations . . . . . . . . . . . . . . . . . . . . . . . . . 291
Jouer avec les lumières . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Les différents types de lumière . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Éclairer une scène pas à pas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Charger un modèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301

CHAPITRE 13
Améliorer le rendu avec le High Level Shader Language . . . . . . 303
Les shaders et XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Vertex shaders et pixel shaders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
Ajouter un fichier d’effet dans XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
Syntaxe du langage HLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306
Les variables HLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306
Les structures de contrôle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Les fonctions fournies pas le langage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Sémantiques et structures pour formats d’entrée et de sortie . . . . . . . . . . . . . 308
Écrire un vertex shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
Écrire un pixel shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Finaliser un effet : les techniques et les passes . . . . . . . . . . . . . . . . . . . . . . . . 310
Créer le fichier d’effet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Faire onduler les objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
La texture en négatif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Jouer avec la netteté d’une texture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Flouter une texture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
Modifier les couleurs d’une texture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319

CHAPITRE A
Visual C# Express 2008 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Différencier solution et projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Personnaliser l’interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
=Labat FM.book Page XX Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


XX

L’éditeur de texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324


Les extraits de code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
Refactoriser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
Déboguer une application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
Raccourcis clavier utiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330

CHAPITRE B
Les bienfaits de la documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
L’incontournable MSDN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Ressources sur le Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Générer de la documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334

Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
=Labat FM.book Page 1 Vendredi, 19. juin 2009 4:01 16

1
Débuter en C#

Ce premier chapitre a pour but de vous guider dans vos premiers pas de programmeurs et
notamment avec le langage C#. Commençant par la découverte des types de données et
allant jusqu’à la création de vos propres classes, ce chapitre constitue le minimum vital à
connaître avant de s’attaquer à la création d’un jeu.
Ne vous inquiétez pas si nous n’avons pas tout de suite recours à XNA, mais commençons
par le mode console. En effet, ce dernier est particulièrement adapté pour l’apprentissage
de C#.

Créez votre premier programme


Avant de nous lancer dans l’apprentissage du C#, découvrons ensemble l’environnement
dans lequel nous allons travailler.
Tout d’abord, démarrez Visual C# Express (figure 1-1).
Ensuite, créez un nouveau projet console en cliquant sur Fichier puis Nouveau Projet
(figure 1-2).
=Labat FM.book Page 2 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


2

Figure 1-1
Accueil de Microsoft Visual C# Express 2008

Figure 1-2
Création
d’un nouveau projet
=Labat FM.book Page 3 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
3

Le logiciel a automatiquement généré le code suivant dans un fichier Program.cs :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
}
}
}
Pour compiler l’application et l’exécuter (lorsque c’est possible), cliquez sur Générer
puis Générer la solution ou utilisez le raccourci clavier F5.
Tout au long de ce chapitre, nous allons analyser les possibilités qu’offre cette portion de
code. Ne fermez surtout pas Visual Studio, vous serez amené à l’utiliser parallèlement à
la lecture de ce chapitre.

Les types de données


Dans cette partie, nous allons dans un premier temps nous intéresser à la manière dont
s’organise globalement le stockage des données en informatique, puis nous passerons
en revue les différents types de données qui nous seront utiles pour la programmation
de jeux.

Organisation de la mémoire
Avant de se jeter tête la première dans le code, il est nécessaire de voir (ou de revoir)
quelques notions de base du fonctionnement de l’ordinateur et tout particulièrement celui
de la mémoire.
Ainsi, vous serez amené à travailler avec les deux grands types de mémoires :
• La mémoire vive – Généralement appelée RAM (Random Access Memory), elle est
volatile. Ce terme signifie qu’elle ne permet de stocker des données que lorsqu’elle est
alimentée électriquement. Ainsi, dès que vous redémarrez votre ordinateur, sa RAM
perd tout son contenu. Lire des données présentes sur ce type de mémoire se fait plus
rapidement que lire des données présentes sur de la mémoire physique.
• La mémoire physique – Cette mémoire correspond à votre disque dur ou à tous les
périphériques physiques de stockage de données (DVD-ROM, carte mémoire, etc.).
Elle n’est pas volatile, son contenu est conservé même lorsqu’elle n’est plus alimentée
électriquement.
=Labat FM.book Page 4 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


4

Sa capacité de stockage est souvent plus élevée que celle de la mémoire vive.
Les informations que l’on stocke dans la RAM s’appellent variables, tout simplement
parce que leur valeur peut changer au cours du temps.

Les variables
Le C# est un langage à typage fort : il existe plusieurs types de variables, chaque type
ayant des caractéristiques bien précises (utilisation mémoire, possibilité, précision, etc.).
En C#, comme dans la vie, on ne mélange pas les torchons et les serviettes.

Stocker des nombres entiers


Les premiers types de variables que nous allons découvrir servent à stocker les nombres
entiers. Leur particularité se situe au niveau de leur capacité de stockage, et donc de leur
occupation en mémoire.

Tableau 1-1 Les types entiers

Type Stockage Valeur minimale Valeur maximale


Byte 1 octet 0 255

Short 2 octets – 32768 32767

Int 4 octets – 231 231-1

Long 8 octets – 9.2 ¥ 1018 9.2 ¥ 1018

Une variable se déclare de la façon suivante :


type identificateur;
Par exemple, pour déclarer une variable entière correspondant au nombre de vies restan-
tes de joueur, il faut procéder de la manière suivante :
short nombreDeVies;
Il faut respecter certaines règles dans le nommage des identificateurs :
• Vous devez faire attention à ce que le premier caractère soit une lettre majuscule ou
minuscule ou un underscore (_).
• Pour tous les autres caractères, vous pouvez utiliser soit une lettre majuscule ou
minuscule, soit un underscore ou alors un chiffre.
À ce stade, la variable étant uniquement déclarée, vous ne pouvez pas l’utiliser. Faites le
test en essayant de compiler le code suivant :
namespace PremierProgramme
{
class Program
{
=Labat FM.book Page 5 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
5

static void Main(string[] args)


{
short nombreDeVies;
Console.WriteLine(nombreDeVies);
}
}
}
Dans ce programme, une variable de type short est déclarée et son contenu s’affiche dans
la console.
Cependant, la compilation a échoué (figure 1-3) et ceci est tout à fait normal. En effet, la
variable a uniquement été déclarée, elle n’a pas été initialisée, c'est-à-dire qu’elle n’a pas
encore reçu de valeur.

Figure 1-3
La compilation du programme a échoué

L’initialisation d’une variable est très facile à réaliser :


identificateur = valeur;
À présent, remplacez le code précédent par celui ci-dessous et compilez-le :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
short nombreDeVies;
nombreDeVies = 7;
Console.WriteLine(nombreDeVies);
Console.ReadLine();
}
}
}
Cette fois-ci, vous constatez que le compilateur ne signale aucune erreur et que la valeur
qui a été affectée à la variable s’affiche correctement dans la console.
Notez que la ligne Console.Read(); qui n’était pas présente dans l’exemple précédent,
permet de figer la console tant que l’utilisateur n’appuie sur aucune touche du clavier.
Sans elle, la fenêtre s’ouvrirait et se fermerait toute seule en un éclair.
=Labat FM.book Page 6 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


6

Déclarer et initialiser une variable peut se faire sur une seule et même ligne :
short nombreDeVies = 7;
Enfin, il est également possible de déclarer et d’initialiser plusieurs variables sur la même
ligne :
short nombreDeVies = 7, score = 0;

Les booléens : vrai ou faux


Une variable de type booléen peut avoir deux états : vrai ou faux, soit respectivement
true ou false. Elle s’utilise de la manière suivante :
bool test = true;
Les booléens sont issus de l’algèbre de Boole. Les conditions et tests logiques sont basés
sur eux.

Découverte des nombres à virgule : les nombres réels


Comme pour les entiers, il existe plusieurs types de variables pour les nombres réels.
Tableau 1-2 Les types réels

Type Stockage Valeur minimale Valeur maximale


float 4 octets 1.4 ¥ 10–45 3.4 ¥ 1038

double 8 octets 4.9 ¥ 10–324 1.8 ¥ 10308

decimal 16 octets 8 ¥ 10–28 8 ¥ 1028

Il faut utiliser le point (.) comme séparateur entre la partie réelle et la partie décimale de
votre nombre. Par exemple :
double nombreReel = 4.56;
Notez qu’en utilisant les types float et double, vous devrez faire face à un problème de
précision. Testez par exemple le programme suivant :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
double total = 0;
while (total < 1)
total += 0.0001;
Console.WriteLine(total);
Console.ReadLine();
}
}
}
=Labat FM.book Page 7 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
7

Le mot-clé while correspond à une structure algorithmique que nous étudierons plus tard.
Ce programme utilise une variable total de type double et l’initialise à 0. Tant que la
valeur totale est inférieure à 1, il faut lui ajouter 0,0001.
À l’exécution, voici ce qui s’affiche dans la console :
1,00009999999991
À présent, changez le type de total et déclarez plutôt la variable en tant que decimal.
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
decimal total = 0;

while (total < 1)


total += (decimal)0.0001;

Console.WriteLine(total);
Console.ReadLine();
}
}
}
Cette fois-ci, voici le resultat qui s’affiche à l’écran :
1
Le type decimal prend plus de place en mémoire que les types float et double, il nécessite
également un temps de traitement plus long.
Dans vos jeux, vous serez souvent amené à manipuler des nombres réels. Lorsque vous
effectuerez des tests sur ces variables, n’oubliez jamais que cette erreur de précision peut
entraîner des erreurs de logique que vous n’auriez pas prévues.
Lorsque vous choisissez le type d’une variable, analysez toujours au préalable vos
besoins et soupesez bien les avantages et inconvénients de chaque possibilité !

Stocker une lettre ou un signe avec char


Pour stocker un caractère, il existe le type char. Celui-ci est codé sur deux octets en
mémoire.
Il s’utilise de la manière suivante :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
=Labat FM.book Page 8 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


8

char lettre = 'a';


Console.WriteLine(lettre);
Console.ReadLine();
}
}
}
Attention à bien utiliser des guillemets simples (ou apostrophes) « ' » et pas des guillemets
doubles « " ».

Les chaînes de caractères


Une variable de type char ne correspond qu’à un seul caractère ; à l’inverse, une chaine
de caractère en contiendra un ou plusieurs. Pour en déclarer une, il faut utiliser le mot-clé
string.
Voici comment l’utiliser :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
string chaine = "Test";
Console.WriteLine(chaine);
Console.ReadLine();
}
}
}
Cette fois-ci, il faudra bien utiliser les guillemets doubles.

Les constantes
Tous les types que nous avons vus jusqu’à maintenant peuvent être déclarés en tant que
constante grâce au mot-clé const. Bien évidemment, et comme son nom l’indique, la
valeur d’une constante ne peut pas être modifiée durant le cycle de vie du programme.
const int N = 7;

Opérations de base sur les variables


Le tableau 1-3 répertorie les opérations arithmétiques de base qui peuvent être utilisées
sur les nombres en C# :
=Labat FM.book Page 9 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
9

Tableau 1-3 Opérateurs de base du langage

Opération Description
A + B Addition de A et de B

A – B Soustraction de B à A

A * B Multiplication de A par B

A / B Division de A par B

A % B Reste de la division de A par B

Le programme suivant met en application ces opérations (le résultat est présenté sur la
figure 1-4) :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
int A = 6;
int B = 3;
Console.WriteLine(A + B);
Console.WriteLine(A - B);
Console.WriteLine(A * B);
Console.WriteLine(A / B);
Console.WriteLine(A % B);
Console.ReadLine();
}
}

Figure 1-4
Test des opérations arithmétiques

Vous pouvez stocker le résultat de chaque calcul dans une variable.


A = A + B;
Ce type d’opération peut également se factoriser de la manière suivante :
A += B;
=Labat FM.book Page 10 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


10

Vous pouvez utiliser ce genre de raccourci avec tous les opérateurs arithmétiques.
Les opérations de pré et post-incrémentations ou décrémentations sont également une
bonne manière de gagner du temps. Leur but est de raccourcir l’écriture de lignes telles
que :
A = A + 1;
En utilisant la post-incrémentation, la ligne précédente devient :
A++;

Tableau 1-4 Opérateurs d’incrémentation et de décrémentation

Opération Description
A++ Post-incrémentation de A

++A Pré-incrémentation de A

A-- Post-décrémentation de A

--A Pré-décrémentation de A

La post-incrémentation se fait après l’exécution d’une ligne d’instruction, alors que la


pré-incrémentation aura lieu avant. Un exemple valant mieux qu’un long discours, compilez
le programme suivant et observez les effets de chacune des opérations (figure 1-5).
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
int A = 6;
Console.WriteLine(A++);
Console.WriteLine(++A);
Console.WriteLine(A--);
Console.WriteLine(--A);
Console.ReadLine();
}
}
}

Figure 1-5
Incrémentation et décrémentation
=Labat FM.book Page 11 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
11

Les instructions de base


Vous ne pouvez pas créer un programme, et a fortiori un jeu, juste en déclarant des variables,
ou si vous y arrivez, le résultat ne serait pas réellement intéressant. Dans cette partie,
nous allons nous intéresser aux instructions de base du langage grâce auxquelles vos
programmes vont se complexifier.

Commenter son code


Que vous soyez amené à partager du code avec d’autres personnes ou non, il est toujours
très important d’être clair et facilement compréhensible lorsque vous programmez. Une
bonne pratique à adopter est donc de commenter votre code.
En C#, il existe trois types de commentaires :
// Commentaire sur une ligne

/*
* Commentaire sur plusieurs lignes
*/

/// Commentaire pour la documentation automatique (voir Annexe A)


S’il est important de commenter votre code, attention cependant à ne pas tomber pas dans
l’excès : ne commentez que ce qui est réellement utile. Essayez d’avoir une vision critique
vis-à-vis de votre code, celle de quelqu’un qui n’a pas mené la réflexion qui vous a fait
aboutir à tel ou tel choix. Ajouter trop de commentaires inutiles risque de rendre vos
fichiers sources illisibles, et de vous faire perdre du temps.

Les conditions : diversifier le cycle de vie des jeux


L’écriture de tests logiques et de conditions est la base de la programmation. Voici la
structure algorithme d’un test simple :
SI CONDITION EST VRAIE
ALORS FAIRE…
FIN SI
En C#, le mot-clé utilisé pour faire un test est le mot-clé if. Voici un exemple d’utilisation
simple :
if (true)
{
Console.WriteLine("Bien!");
}
Si le code à exécuter dans le cas où la condition est vraie et ne tient que sur une ligne, il
est également possible d’écrire :
if (true)
Console.WriteLine("Bien!");
=Labat FM.book Page 12 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


12

Mais ce test n’a pas réellement d’intérêt. Dans l’exemple suivant, le test porte sur le
nombre de vies restantes à un joueur. S’il n’en a plus, le programme lui signale qu’il est
mort.
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
short nombreDeVies = 1;

if (nombreDeVies == 0)
Console.WriteLine("Vous êtes mort");

nombreDeVies--;

if (nombreDeVies == 0)
Console.WriteLine("Vous êtes mort");

Console.ReadLine();
}
}
}
Voici la liste des opérateurs conditionnels :

Tableau 1-5 Opérateurs conditionnels

Opérateur Description
== Test d’égalité

!= Test de différence

> Strictement supérieur

>= Supérieur ou égal

< Strictement inférieur

<= Inférieur ou égal

Si le test n’est pas concluant, il est possible d’effectuer d’autres actions.


SI CONDITION EST VRAIE
ALORS FAIRE…
SINON FAIRE…
FIN SI
En C#, l’instruction correspondant au terme SINON est l’instruction else. Compilez le
programme suivant pour tester cette notion.
=Labat FM.book Page 13 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
13

short nombreDeVies = 1;

if (nombreDeVies == 0)
Console.WriteLine("Vous êtes mort");
else
Console.WriteLine("Vous êtes encore en vie !");
Il est possible d’ajouter un nombre infini de conditions. Ceci donne le code suivant :
SI CONDITION EST VRAIE
ALORS FAIRE…
SINON SI CONDITION EST VRAIE FAIRE…
SINON FAIRE…
FIN SI
Tout ceci se traduit en C# par else if. Voici un nouvel exemple :
short nombreDeVies = 1;

if (nombreDeVies == 0)
Console.WriteLine("Vous êtes mort");
else if (nombreDeVies == 1)
Console.WriteLine("Vous êtes bientôt mort...");
else
Console.WriteLine("Vous êtes encore en vie !");
Vous avez à présent assez de connaissances pour effectuer un programme qui réagit aux
choix de l’utilisateur. Le code suivant demande à l’utilisateur son genre et affiche un
message en conséquence (figure 1-6).
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
string genre;

Console.WriteLine("Entrez votre genre (M/F) :");


genre = Console.ReadLine();

if (genre == "M")
Console.WriteLine("C'est noté Monsieur !");
else if (genre == "F")
Console.WriteLine("Bonjour Mademoiselle...");
else
Console.WriteLine("Oh... C'est vrai ?");

Console.ReadLine();
}
}
}
=Labat FM.book Page 14 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


14

Figure 1-6
Vos programmes se compliquent…

Vous pouvez combiner plusieurs tests à la fois grâce aux opérateurs logiques.

Tableau 1-6 Opérateurs logiques

Opérateur Description
&& Et

|| Ou

! Non

Ces opérateurs vous permettent de factoriser votre code. Ainsi le test suivant…
if(A > B)
if(A > C)
Console.WriteLine("A est le plus grand.");
… peut s’écrire de cette manière :
if(A > B && A > C)
Console.WriteLine("A est le plus grand.");
Dans l’exemple suivant, l’opérateur Not donne raison à Jimi Hendrix en inversant le résultat
d’un test.
if(!(6 == 9))
Console.WriteLine("If 6 was 9");
Il existe d’autres instructions de condition que vous découvrirez étape par étape dans la
suite de cet ouvrage.

Les fonctions
Dans cette partie vous apprendrez à factoriser votre code et à le rendre réutilisable en
utilisant les fonctions et les procédures.
=Labat FM.book Page 15 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
15

Différencier fonction et procédure


Vous ne pouvez pas écrire tout un programme ou tout un jeu, même s’il contient peu de
lignes de codes, dans le même fichier : cela serait illisible et impossible à maintenir. Vous
feriez mieux d’utiliser les fonctions et les procédures.
Une fonction exécute certaines actions (des calculs, de l’extraction de données, etc.), puis
renvoie un résultat. Par exemple, la fonction Carre() attend un nombre en argument, et
renvoie ce même nombre élevé au carré. Dans un jeu, vous pourriez avoir une fonction
GetFriends() qui retournerait la liste des amis de votre personnage.
Contrairement à une fonction, une procédure ne retournera pas de résultat. Ainsi, la
procédure AfficherMenu() affichera un menu dans la console, mais ne retournera pas de
valeur. Dans votre jeu, la procédure Draw() contient les mécanismes de dessin de votre
jeu, mais ne retourne pas de valeur.
Utiliser une fonction ou une procédure vous permet de gagner du temps en factorisant
le nombre de lignes de code que vous écrivez. De plus, créer des fonctions ou des procé-
dures offre l’avantage non négligeable d’écrire du code réutilisable. Ainsi, au fur et à
mesure de vos projets, vous vous construirez une véritable bibliothèque de « briques »
réutilisables.

Écrire une première procédure


Tout d’abord, sachez que jusqu’ici vous avez déjà utilisé plusieurs procédures, peut-être
à votre insu si ce livre constitue votre première expérience de programmation.
L’extrait de code ci-dessous comporte la fonction principale d’un programme. Si vous
l’exécutez, le message « Bonjour »s’affiche dans la console, puis dès que vous appuyez
sur une touche, la console se ferme.
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Bonjour");
Console.ReadLine();
}
}
}
Dans cette petite portion de code se cachent deux procédures. La procédure Main, que
vous définissez et la procédure WriteLine, que vous appelez.

En pratique
Main est le point d’entrée de l’application : c’est ici que tout commence.
=Labat FM.book Page 16 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


16

Appeler une procédure se fait donc très simplement, il suffit d’écrire son nom, et si
besoin, de lui passer des arguments. Dans l’exemple précédent, vous appelez WriteLine
en lui passant une variable de type string, qui correspond au texte à afficher dans la
console.
Il est temps d’écrire votre première procédure. Celle-ci servira à afficher quelques lignes
de Lorem Ipsum (faux texte bien connu des développeurs web).
La déclaration d’une procédure se fait de la manière suivante :
Void NomDeLaProcedure(typeA argumentA, ...)
{
}
Le mot-clé void signifie qu’il n’y a pas de valeur en retour, ce qui correspond bien à la
définition d’une procédure.
Le nom d’une procédure est régi par les mêmes règles qui s’appliquent aux noms de
variables. Le nombre d’arguments à passer à la fonction dépend bien évidemment de vos
besoins. Sachez qu’il est également possible de passer les arguments par référence plutôt
que par valeur. Pour l’instant tous les passages que nous allons voir se font par valeur
(nous aborderons les autres plus tard).
Dernière règle à respecter lors de la déclaration d’une procédure : son corps doit être
entouré de deux accolades ouvrante « { » et fermante « } ».
Voici donc la définition et, bien sûr, l’appel de cette première procédure :
namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
AfficherLoremIpsum();
Console.ReadLine();
}

static void AfficherLoremIpsum()


{
Console.WriteLine("Lorem ipsum dolor sit amet, consectetuer adipiscing
➥ elit.");
Console.WriteLine("Aliquam pretium, leo non scelerisque porttitor,
➥ tellus turpis feugiat lacus, sed ullamcorper nisl felis non nibh.");
Console.WriteLine("Fusce posuere mollis justo.");
}
}
}
=Labat FM.book Page 17 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
17

Renvoi
Remarquez la présence du mot-clé static devant void. Nous reviendrons sur la signification de ce mot-
clé dans la section « Créer une classe » de ce chapitre.

Écrire une première fonction


Depuis le début de la lecture de ce livre, vous utilisez la fonction ReadLine. Celle-ci lit
l’entrée clavier et renvoie une chaîne de caractères. Il est donc possible d’imaginer sa
définition :
string ReadLine()
{
// ...
return valeur;
}
Les règles pour la définition d’une fonction sont les mêmes que celles qui s’appliquent
aux procédures, sauf pour le type. En effet, vous n’êtes pas restreint au type void, mais
vous pouvez utiliser celui que vous voulez.
Le code suivant contient la définition d’une fonction qui renvoie la valeur absolue d’un
nombre passé en argument, ainsi que l’utilisation de cette fonction :
namespace PremierProgramme
{
class Program
{
static int ValeurAbsolue(int number)
{
if (number < 0)
return -number;
else
return number;
}

static void Main(string[] args)


{
int a = ValeurAbsolue(-45);
Console.WriteLine(a);

Console.WriteLine(ValeurAbsolue(5));

Console.ReadLine();
}
}
}
Cet exemple n’a de but autre que pédagogique. Sachez que plusieurs milliers de fonctions
sont fournies par le framework .NET, répondant à des besoins extrêmement variés. Vous
=Labat FM.book Page 18 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


18

avez également la possibilité de vous procurer des fonctions spécifiques sur l’Internet ou
bien de créer les vôtres et les communiquer à d’autres développeurs.
Lorsque vous utilisez une procédure ou bien une fonction, Visual Studio vous fournit des
informations sur le type de la fonction ou les paramètres attendus (figure 1-7).

Figure 1-7
Visual Studio sait se rendre très utile

Les classes
Piliers du C# et donc de XNA, les classes et les objets sont indispensables dans la création
de n’importe quel programme : chaque programme écrit en C# (qu’il s’agisse d’un jeu,
d’une application Windows, d’une application console, etc.) en possède au moins une.
Dans cette section, nous nous intéresserons tout d’abord à la différence entre une classe et
un objet. Nous pourrons alors voir comment utiliser un objet, et enfin, écrire votre première
classe.

Comprendre les classes et les objets


Avant de nous lancer dans les aspects techniques, il est nécessaire de bien cerner les
notions de classes et d’objets.
Pour prendre un exemple concret, une classe est comparable à une recette de gâteau et un
objet à un gâteau. En somme, en réalisant un gâteau, vous avez donné vie à votre recette.
En programmation orientée objet (POO), l’objet gâteau est alors qualifié d’instance de la
classe recette de gâteau.
Un objet possède des propriétés et des méthodes (il peut s’agir de fonctions ou de procé-
dures). Le tableau ci-dessous liste celles d’un objet instancié à partir de la classe Homme.
Tableau 1-7 Différence entre propriétés et méthodes

Propriétés Méthodes
Taille, force, agilité, etc. Marcher, boire, se défendre, etc.

Utiliser un objet
Vous avez déjà utilisé des objets sans le savoir. Derrière les types de données que nous
avons vus précédemment se cachent des classes. Ainsi, pour déclarer une chaîne de
caractères, il est également possible d’écrire :
String chaine = "Test";
=Labat FM.book Page 19 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
19

String (avec un S majuscule) correspond donc à la classe.


Les propriétés et méthodes d’un objet String sont visibles en inscrivant un point (.), puis
en choisissant ce à quoi vous voulez accéder.
Console.WriteLine(chaine.Length); // Longueur de la chaîne
Console.WriteLine(chaine.Substring(1, 2)); // Sous-chaîne commençant à la position 1
➥ et d'une longueur de 2 caractères
Dans le framework .Net, un grand nombre de classes est disponible et répondront à bon
nombre de vos besoins : File pour la gestion de fichiers, Directory pour la gestion de
répertoire, Socket pour les communications réseaux, etc. Prenons, par exemple, la classe
TimeSpan, qui représente un intervalle de temps :
TimeSpan time = new TimeSpan(26, 14, 30, 10);
Console.WriteLine(time.ToString());

L’initialisation d’un objet se fait via le mot-clé new, qui appelle le constructeur de la
classe TimeSpan. Un constructeur est une fonction un peu spéciale, nous y reviendrons lors
de la création de votre première classe.

Cas particulier
Si vous parcourez un peu les classes fournies par le framework (en utilisant IntelliSense, reportez-vous
l’annexe A pour plus de détails), vous découvrirez sûrement la classe Math, qui est un peu spéciale. En
effet, la création d’un objet de type Math est impossible. En fait, cette classe est statique, c'est-à-dire
qu’elle n’est pas instanciable. Cependant, elle possède tout de même des propriétés et des méthodes,
elles aussi statiques, qui sont accessibles de la manière suivante :
Console.WriteLine(Math.PI);
Console.WriteLine(Math.Abs(-15));

Qu’est ce qu’un espace de noms ?


Un espace de noms (namespace) organise les classes de manière logique : tout comme les
répertoires permettent de classer les fichiers, les espaces de noms servent à organiser les
classes.
Ainsi, il ne peut y avoir deux classes du même nom dans le même espace de noms.
Cependant, deux classes peuvent tout à fait porter le même nom si elles ne sont pas dans
le même espace.
La directive using sert à définir des alias pour rendre plus facile l’identification des espaces
de noms ou des classes. Elle permet aussi d’accéder à des types sans avoir à préciser à
chaque fois l’espace auquel elles appartiennent.
=Labat FM.book Page 20 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


20

Analysons le programme suivant :


using System;

namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Math.PI);
Console.ReadLine();
}
}
}
La classe Math et la classe Console sont contenues dans l’espace de noms System. Si nous
supprimions la ligne qui contient la directive using, le programme ne pourrait plus être
compilé puisqu’il ne saurait plus à quoi correspondent les noms Math et Console. Il
faudrait donc le récrire de la manière suivante :
System.Console.WriteLine(System.Math.PI);
System.Console.ReadLine();
Le code ci-dessous présente la directive using dans la définition d’alias.
using A = System.Console;
using B = System.Math;

namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
A.WriteLine(B.PI);
A.ReadLine();
}
}
}

Créer une classe


Il est temps à présent d’écrire une première classe. Cliquez sur Projet, puis sur Ajouter
une classe. Nommez le fichier Humain.cs (figure 1-8).
=Labat FM.book Page 21 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
21

Figure 1-8
Création d’une nouvelle classe

Voici le code de base généré par Visual Studio :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PremierProgramme
{
class Humain
{
}
}
Nous reconnaissons la syntaxe de déclaration d’une classe. Vous notez également que
toute classe doit être contenue dans un espace de noms. Ici, l’espace de noms correspond
au nom de notre projet.
L’une des choses les plus importantes dans la création d’une classe est la notion d’encap-
sulation. Derrière ce terme, se cache la notion de droits d’accès aux éléments d’une classe.

Tableau 1-8 Visibilité d’un élément

Droit Description
Public Accessible depuis l’extérieur de l’objet

Private Accès restreint à l’objet


=Labat FM.book Page 22 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


22

Il existe d’autres mots-clés pour les droits d’accès que vous rencontrerez dès le chapitre 2.
Si vous ne précisez aucun mot-clé, la valeur par défaut est private.
Rappelez-vous, nous avons vu précédemment que pour pouvoir instancier un objet, une
classe doit posséder un constructeur. Un constructeur a la syntaxe suivante :
droit NomDeLaClasse(typeA argumentA, ...)
{
}
Pour que le constructeur soit accessible, il faut donc le déclarer en temps que public.
namespace PremierProgramme
{
class Humain
{
public Humain()
{
}
}
}
Il est dès maintenant possible de créer un objet en instanciant la classe Humain. Ouvrez
le fichier Program.cs et procédez aux modifications nécessaires.
static void Main(string[] args)
{
Humain heros = new Humain();
Console.ReadLine();
}

Thème avancé
Les classes partielles sont des classes dont la définition est fractionnée entre plusieurs fichiers. Pour créer
une telle classe, il faut utiliser le mot-clé partial :
Public partial class ClassTest
{
}
Depuis l’arrivée du framework .Net 2.0, le designer d’interface de Visual Studio utilise les classes partielles
pour séparer le code qu’il génère de celui que vous écrivez.

Il est temps de rendre les choses un peu plus excitantes et de donner un nom à votre humain,
ainsi que la possibilité de se présenter. Ajoutez donc un champ à notre classe Humain,
mais ne le déclarez pas public.
C# fournit un mécanisme très souple pour la lecture (get) ou l’écriture (set) dans les
champs d’une classe. Ce mécanisme relève des propriétés, et en voici la syntaxe :
public type Nom
{
get { return variable; }
=Labat FM.book Page 23 Vendredi, 19. juin 2009 4:01 16

Débuter en C#
CHAPITRE 1
23

set { variable = value; }


}
Mais, pour appliquer ce concept à notre cas d’étude, il faut rester logique. En effet,
implémenter l’opérateur d’écriture n’est peut être pas nécessaire puisqu’il est très rare
qu’un humain puisse changer de nom.
public string Nom
{
get { return nom; }
}
Dernière chose à faire, le nom du personnage doit lui être attribué à sa création. Il faut
donc ajouter un argument au constructeur et initialiser le champ.
public Humain(string nom)
{
this.nom = nom;
}
Un problème se pose ici. En effet, le nom de l’argument attendu par le constructeur (nom)
est le même que le nom du champ à initialiser. Dans ce cas, l’usage du mot-clé this
permet alors de désigner l’instance courante de la classe, et ainsi accéder au champ nom et
pas à l’attribut du constructeur.
Si, à ce stade, vous essayez de compiler le programme, vous obtiendrez une erreur :
'PremierProgramme.Humain' ne contient pas de constructeur qui accepte des arguments
➥ '0'
En effet, votre constructeur attend un argument. Or vous ne lui en passez aucun lors de la
création de votre objet. Retournez donc dans le fichier Program.cs et modifiez la ligne
comme bon vous semble.
Humain heros = new Humain("Moi");
Vous pouvez afficher le nom de notre humain en utilisant la propriété que vous avez définie
plus tôt :
Console.WriteLine(heros.Nom);
Pour finir, écrivez une méthode appelée « SePresenter » à la classe Humain. Elle a pour
fonction d’afficher une petite phrase de présentation dans la console. Puis, utilisez-la
dans la fonction Main. Voici ci-dessous le code source final de votre classe.
Humain.cs
namespace PremierProgramme
{
class Humain
{
string nom;
public string Nom
{
=Labat FM.book Page 24 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


24

get { return nom; }


}

public Humain(string nom)


{
this.nom = nom;
}

public void SePresenter()


{
System.Console.WriteLine("Bonjour, je m'appelle " + nom);
}
}
}

En pratique
L’opérateur + sert à concaténer plusieurs chaînes de caractères.

Program.cs
using System;

namespace PremierProgramme
{
class Program
{
static void Main(string[] args)
{
Humain heros = new Humain("Moi");
heros.SePresenter();
Console.ReadLine();
}
}
}

En résumé
Vous disposez à présent du bagage de connaissances nécessaires à la compréhension des
bases de XNA, à savoir :
• choix et utilisation de types de données ;
• écriture de structures conditionnelles ;
• écriture et utilisation de fonctions ou de procédures ;
• compréhension des bases de la programmation orientée objet, des classes et des objets.
Dans le chapitre suivant, vous allez appliquer les notions que nous venons de voir, et
vous ferez vos premiers pas dans l’univers de la création de jeu.
=Labat FM.book Page 25 Vendredi, 19. juin 2009 4:01 16

2
Prise en main de XNA

Connaissez-vous réellement les possibilités qu’offre le framework XNA ? Ce chapitre les


présente afin que vous vous rendiez compte de quoi vous serez capable après quelques
heures de pratique avec XNA.
Après avoir lu ce premier chapitre, vous serez en mesure de créer votre premier projet, de
comprendre les différents éléments qui le composent et de le déployer sur une Xbox 360.

Télécharger l’EDI et XNA


Si l’EDI Microsoft Visual C# Express 2008 et le framework ne sont pas déjà sur votre
ordinateur, voici la procédure à suivre pour vous en équiper :
1. Téléchargez Microsoft Visual C# Express 2008 en vous rendant à cette adresse :
http://www.microsoft.com/express/download/

Figure 2-1
Téléchargement de Microsoft
Visual C# Express 2008
=Labat FM.book Page 26 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


26

2. Une fois le fichier téléchargé, exécutez-le pour démarrer l’installation.


3. Il ne reste plus qu’à télécharger et installer XNA en vous rendant sur le site officiel à
cette adresse :
http://creators.xna.com/en-us/xnags_islive

Partir d’un starter kit


En général, les starter kits sont des projets de jeux prêts à l’emploi et à compiler. Ils sont
faciles à modifier et constituent une bonne base pour vos projets : vous pouvez analyser
leur code source, en utiliser une partie dans vos jeux et le modifier.
Penchons-nous sur celui livré avec XNA 3.0 :
1. Ouvrez Visual Studio.
2. Cliquez sur Fichier puis Nouveau projet.
3. Dans la section XNA Game Studio 3.0, sélectionnez Platformer Starter Kit (3.0).
4. Validez.

Figure 2-2
Création d’un projet basé sur un starter kit

Dans l’explorateur de solutions situé à droite de l’écran, vous trouvez trois projets : un
pour Windows, un pour la Xbox 360 et le dernier pour Zune.
=Labat FM.book Page 27 Vendredi, 19. juin 2009 4:01 16

Prise en main de XNA


CHAPITRE 2
27

Zune
Zune est le lecteur multimédia de Microsoft, concurrent de l’iPod d’Apple. Depuis la version 3.0 d’XNA, il
est possible de développer des jeux sur cette plate-forme.
Toutefois, sachez qu’à l’heure où nous écrivons ces lignes, Zune est commercialisé uniquement aux États-
Unis et qu’aucune date officielle n’a été annoncée pour une éventuelle apparition sur le marché français.

Appuyez sur F5 pour lancer la compilation du projet sélectionné par défaut (ici, le projet
Platformer).

Figure 2-3
Le jeu est agréable à jouer.

Le petit jeu qui s’ouvre alors est un bon exemple de ce que vous pouvez facilement réaliser
avec XNA : afficher des graphismes, déclencher des sons et enchaîner plusieurs niveaux.
D’autres starter kits sont disponibles et téléchargeables sur le site Internet de la commu-
nauté d’XNA : http://creators.xna.com/en-US/education/starterkits/
Jeux de rôles, shoot’em up, jeux de course et puzzles : il suffit de jeter un œil à la liste des
kits pour voir que les possibilités de créations avec XNA sont presque illimitées !
Nous vous conseillons de prendre le temps d’explorer plus en détail les kits, et notamment
de regarder leur code. Vous reconnaîtrez sûrement les facettes du langage abordées dans le
chapitre précédent, mais certaines parties du code vous sembleront au contraire obscures :
ceci est tout à fait normal pour le moment. Cependant, revenir sur les kits plus tard peut
=Labat FM.book Page 28 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


28

être intéressant et très instructif, ne serait-ce que pour comparer vos méthodes de
programmation avec celle d’un autre développeur, ou encore pour savoir comment telle
ou telle partie du jeu a été réalisée.

Partager ses projets


Les starter kits restent des projets assez simples et leur vocation est essentiellement
didactique. La plupart d’entre eux sont basés sur des jeux en 2D. Mais rassurez-vous, il
est tout à fait possible de réaliser des jeux en 3D avec XNA.
Pour vous en convaincre, il vous suffit de faire un petit tour sur le site de la communauté
(http://creators.xna.com/en-US/) et de vous intéresser aux projets des autres membres. En
effet, sur ce site, vous pourrez parcourir le catalogue des jeux développés par des amateurs,
des développeurs patentés, voire des studios indépendants. Vous pourrez également
visualiser des vidéos de présentation et même en acheter certains.

Figure 2-4
Le catalogue de jeux disponible sur le site de la communauté

Nous conseillons vivement de vous y inscrire et de participer aux forums de discussion,


car c’est le meilleur endroit pour obtenir de l’aide sur XNA et, d’une manière générale,
sur la programmation de jeux.
=Labat FM.book Page 29 Vendredi, 19. juin 2009 4:01 16

Prise en main de XNA


CHAPITRE 2
29

Encouragement
Au moment où nous rédigeons ce livre, il n’existe pas de lieu de rassemblement pour la communauté fran-
cophone. Cependant, nous espérons que l’anglais n’est pas une barrière infranchissable pour vous. En
effet, vous ne pourrez pas y échapper (même si vous réussissez à vous procurer des ouvrages en français
tels que celui-ci), et il y a de fortes chances qu’à un moment ou un autre vous deviez échanger avec un
interlocuteur étranger.

Vous aurez sûrement envie de partager vos projets. Ceci est intéressant à plusieurs titres :
vous pourrez ainsi présenter vos créations à vos amis, mais surtout, vous récolterez par ce
biais les avis et conseils des autres développeurs.
Avec la sortie de XNA 3.0, la possibilité de vendre ses jeux sur le Xbox Live est apparue.
Votre jeu est alors disponible sur le Xbox Live Market pour quelques centaines de points
Microsoft. Les détails sont disponibles sur le site Internet de la communauté (http://creators
.xna.com/).
Une autre solution pour faire connaître vos talents de développeur et vous frotter aux
autres afin de progresser est de participer aux concours de programmation XNA. Citons
par exemple l’Imagine Cup, organisé chaque année par Microsoft et possédant une caté-
gorie intitulée Game Development, dont les finalistes gagnent plusieurs milliers de dollars
(http://imaginecup.com).

L’architecture d’un projet XNA


Dans cette partie, nous allons d’abord nous intéresser aux différents éléments du framework,
puis nous nous pencherons sur les différentes méthodes qui composent le cycle de vie
d’un jeu vidéo sous XNA.

Structure du framework
Le framework XNA comporte essentiellement trois parties, chacune correspondant à une
DLL (Dynamic Link Library, c’est-à-dire une bibliothèque de fonctions) :
• le moteur graphique (Microsoft.XNA.Framework.dll), qui contient tout qu’il faut pour
gérer l’affichage dans votre jeu ;
• le modèle d’application d’un jeu (Microsoft.XNA.Framework.Game.dll), que nous
détaillerons dans la section « Structure du code » ;
• et le content pipeline (Microsoft.XNA.Framework.Content.Pipeline.dll), utile à la gestion
des ressources (texture, son, etc.) du jeu.
Les fonctions contenues dans ces bibliothèques font appel à des fonctions de DirectX de
plus bas niveau, c’est-à-dire qu’à une ligne de code utilisant le framework XNA corres-
pondent plusieurs lignes de code utilisant directement DirectX.
=Labat FM.book Page 30 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


30

Dans Visual Studio, vous pouvez voir à quelles bibliothèques votre projet est lié dans la
section References de l’explorateur de solutions.

Structure du code
Le déroulement d’un jeu sous XNA est le suivant : les méthodes Initialize() et LoadContent()
sont appelées en premier puis, tant que le joueur ne quitte pas le jeu, les méthodes Update()
et Draw() sont exécutées en boucle ; enfin, lorsque le joueur quitte le jeu, la méthode
UnloadContent() est appelée. La liste ci-dessous détaille les différentes actions à effectuer
dans chacune de ces cinq méthodes.
• Initialize – Comme son nom l’indique, c’est dans cette méthode que se font tous les
réglages de base : instanciation d’un objet, chargement de paramètres, etc.
• LoadContent et UnloadContent – Si vous suivez le modèle d’application proposé par
Microsoft, c’est ici que vous chargerez ou déchargerez vos ressources. Cependant,
certains programmeurs ont tendance à réaliser ce travail avec la méthode Initialize().
• Update – Cette méthode fait partie de la boucle de jeu. D’après le modèle d’application
proposé par Microsoft, c’est ici que vous devrez effectuer toutes les opérations dites
logiques, c’est-à-dire tout ce qui ne concerne pas l’affichage à l’écran.
• Draw – Cette dernière méthode, qui fait également partie de la boucle de jeu, est appelée
à chaque fois que l’écran de jeu est mis à jour. Vous devrez donc y écrire uniquement
du code utile à l’affichage.
Souvenez-vous que la séparation du code entre les méthodes Update() et Draw() n’est
absolument pas obligatoire. Il s’agit d’une proposition de design faite par les créateurs
d’XNA afin que les développeurs utilisant XNA puissent facilement récupérer des compo-
sants créés par d’autres et mettre les leurs à disposition (les composants seront abordés au
chapitre 4). Nous suivrons cette recommandation dans ce livre, le modèle étant très simple
à comprendre et le code créé très bien organisé de cette manière.

Créer un projet
Le temps est maintenant venu de débuter notre premier projet. Dans Visual Studio cliquez
sur Fichier puis sur Nouveau projet. Sélectionnez Windows Game (3.0), puis validez.
Ouvrez le fichier Game1.cs. Vous reconnaissez l’architecture que nous venons de voir et,
grâce aux commentaires, vous comprenez ce que fait notre programme à chaque appel de
Update() et de Draw().
Dans le fichier Program.cs, vous retrouvez la fonction Main() de notre programme. C’est
ici que notre objet Game1 s’instancie, et que le lancement du jeu a lieu via la méthode
Run().
=Labat FM.book Page 31 Vendredi, 19. juin 2009 4:01 16

Prise en main de XNA


CHAPITRE 2
31

Figure 2-5
Création d’un projet pour Windows

En compilant notre programme, vous constaterez qu’il ne s’agit que d’une simple fenêtre
avec un fond bleu. Si vous le lisez sur Xbox, vous aurez également la possibilité d’utiliser
le bouton Back de la manette pour quitter.
À chaque appel de la méthode Draw(), la ligne de code suivante va se charger d’effacer
puis de coloriser l’écran :
GraphicsDevice.Clear(Color.CornflowerBlue);

Attention ! Color n’est pas une classe mais une structure. La différence entre structure et
classe est la suivante :
• une classe se manipule par une référence ;
• une structure se manipule par sa valeur.
Nous avons déjà rencontré d’autres structures lors du chapitre sur C#. Ainsi, les types int
et double, pour ne citer que deux exemples, font partie des structures.
Voyons à présent comment modifier ce programme de base et changeons la couleur de la
fenêtre. Pour cela, effacez l’argument passé à la méthode Clear, puis récrivez « Color. ».
Lorsque vous tapez « . », une petite fenêtre s’ouvre, affichant tout ce que contient la
structure Color (figure 2-6). Choisissez alors la couleur que vous désirez.
=Labat FM.book Page 32 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


32

Figure 2-6
Une première fenêtre avec XNA

En pratique
Ce mécanisme s’appelle IntelliSense. Il s’agit du système d’autocomplétion de Microsoft qui, en plus de
vous aider dans l’écriture du code, vous fournit de la documentation sur les classes, fonctions, etc. Son
fonctionnement est repris plus en détail dans l’annexe A.

Figure 2-7
Choix d’une couleur grâce à IntelliSense

Si vous avez regardé le code de base d’un peu plus près, vous avez peut-être remarqué la
présence d’un objet graphics de type GraphicsDeviceManager. C’est cet objet qui gère les
traitements graphiques du jeu. Ainsi, vous pouvez facilement redimensionner votre
application.
=Labat FM.book Page 33 Vendredi, 19. juin 2009 4:01 16

Prise en main de XNA


CHAPITRE 2
33

public Game1()
{
graphics = new GraphicsDeviceManager(this);
this.graphics.PreferredBackBufferHeight = 100;
this.graphics.PreferredBackBufferWidth = 100;
Content.RootDirectory = "Content";
}
Ou encore, la faire démarrer en mode plein écran.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
this.graphics.ToggleFullScreen();
Content.RootDirectory = "Content";
}

S’outiller pour développer sur Xbox 360


Si vous souhaitez développer pour la Xbox 360, vous devez disposer d’un abonnement
Premium au XNA Creators Club. Cet abonnement vous coûtera 49 pour 4 mois ou 99
pour un an. Si vous êtes étudiant, vous avez accès à une version d’essai de l’abonnement
premium. Renseignez-vous auprès de votre structure enseignante.
Il faut également configurer votre Xbox pour transférer vos jeux depuis votre PC :
1. Depuis votre console, connectez-vous à Xbox Live, puis téléchargez XNA Game
Studio Connect.
2. Rendez-vous ensuite dans votre bibliothèque de jeu, puis dans la section « Jeux de la
communauté » et lancez l’application que vous venez de télécharger.
3. La première fois que vous lancez cet utilitaire, vous voyez un code apparaître à
l’écran, notez-le.
4. Sur votre ordinateur, lancez l’application XNA Game Studio Device Center, soit en
allant la chercher dans le répertoire XNA Game Studio 3.0, soit à partir de Visual
Studio (menu Outils).
5. Cliquez sur Add Device, choisissez Xbox 360, entrez le nom que vous voulez donner
à votre console, puis insérez le code que vous avez récupéré auparavant.
Vous pouvez maintenant déployer votre projet sur votre Xbox. Pour ce faire, assurez-vous
que vous avez bien démarré XNA Game Studio Connect sur la console et compilez le jeu
dans Visual Studio. Le reste se fait automatiquement et votre jeu devrait apparaître sur la
console.
Si vous avez créé un projet pour Windows et que vous voulez finalement le lire sur votre
Xbox, il vous suffit de faire un clic droit sur le projet dans l’explorateur de solutions de
=Labat FM.book Page 34 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


34

Visual Studio et de choisir Create Copy for Xbox 360. Le projet est alors prêt à être
compilé pour la console.

Figure 2-8
Création d’une copie de projet pour la Xbox 360

Développer un jeu pour la console ne se fait pas totalement de la même manière que pour
Windows (notamment à cause de la diversité des moniteurs TV). Vous trouverez un guide
des bonnes pratiques sur le site de la communauté :
http://creators.xna.com/en-US/education/bestpractices

En résumé
Dans ce chapitre, vous avez découvert :
• les types de jeux que vous pouvez créer avec XNA ;
• comment télécharger des jeux ou partager les siens avec la communauté ;
• la structure du framework et d’un projet avec XNA ;
• comment développer un jeu pour la Xbox 360.
=Labat FM.book Page 35 Vendredi, 19. juin 2009 4:01 16

3
Afficher et animer
des images : les sprites

Pong, Super Mario Bros, Sonic, Zelda… quel est le point commun entre ces jeux vidéo ?
Des années 1970 jusqu’au début des années 1990, leurs graphismes 2D ont marqué à
jamais l’industrie du jeu vidéo. À l’heure où le nombre de jeux dits casuals explose et où
le gameplay compte plus que les graphismes, il est clair que la 2D a encore de beaux
jours devant elle… surtout qu’elle n’a jamais été aussi simple à utiliser qu’avec XNA !
En appliquant directement les concepts que vous venez de découvrir, ce chapitre constitue
une introduction à la programmation de jeu 2D.

Définition
Le terme casual (en français, occasionnel) caractérise un jeu, dont les mécanismes sont assez basiques,
pouvant être pris facilement en main par tout le monde, y compris ceux qui découvrent les jeux vidéo
(notamment les personnes âgées).
Les exemples les plus célèbres sont le Solitaire (livré avec Windows), Tetris ou, plus récemment, Wii Sport.

Les sprites
Dans cette première partie, vous allez découvrir l’élément de base de tout jeu en 2D : le
sprite. Vous apprendrez ce qu’est un sprite et comment l’utiliser.
=Labat FM.book Page 36 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


36

Qu’est-ce qu’un sprite ?


De quoi est constitué le jeu Pong ? De deux raquettes et une balle, soit trois formes à
afficher (figure 3-1). On appelle ces formes des sprites et non des images. Pourquoi ?
Une image n’est qu’un tableau de pixels. Mais dans le cas de ce jeu, pour afficher une
raquette, vous ne pouvez pas vous contenter de son image, il faut également lui donner
une position sur l’écran : c’est le rôle du sprite. Il englobe une image et des informations
relatives à son affichage.

Figure 3-1
En 1972, Pong est le
premier gros succès du jeu
vidéo

L’intérêt de différencier les notions d’image et de sprite permet également d’économiser


des ressources. Imaginez que vous deviez programmer un Pong sur un système ne permettant
de stocker que deux images en mémoire.
Pour pouvoir tout de même afficher trois sprites, vous devrez charger une image de
raquette et une image de balle en mémoire, puis créer trois sprites en précisant à chacun
d’eux l’adresse en mémoire de l’image à laquelle ils doivent être liés. Les deux sprites
correspondants aux deux raquettes seront donc liés à une même image (figure 3-2).

Figure 3-2
Les deux sprites utilisent la
même image

Bien sûr, les contraintes sont volontairement exagérées dans cet exemple. Cependant,
dans le cas d’un jeu devant afficher cent fois la même image, imaginez l’espace mémoire
qui serait utilisé si elle était chargée autant de fois !
=Labat FM.book Page 37 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
37

Sur la figure 3-3, vous pouvez voir un jeu tile-based, c’est-à-dire que l’environnement
représenté est composé de tiles (tuiles ou cases en français). Ce genre de jeu a été rendu
célèbre grâce à des titres comme Zelda ou Tales of Phantasia.

Figure 3-3
Les jeux tile-based utilisent de nombreuses fois les mêmes images

Le composant, que vous développerez à la fin de ce chapitre, aura pour tâche de mettre en
mémoire les images requises pour l’affichage de la scène, puis de les lier avec les sprites
qui en ont besoin.

Classe sprite
Il n’existe pas de classe sprite de base dans XNA. Vous en coderez une dans ce chapitre.

Afficher un sprite
Avant de charger l’image, il faut commencer par l’ajouter au projet. XNA possède un
dispositif appelé Content Manager grâce auquel vous allez pouvoir importer et charger
des fichiers sans aucun problème. Pour ce chapitre, l’image qui sera utilisée est un fichier
PNG qui représente la balle d’un Pong (un simple carré blanc de 16 ¥ 16 pixels).
=Labat FM.book Page 38 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


38

Ajouter un fichier au Content Manager se fait de la même manière qu’ajouter un fichier


source à un projet :
1. Dans l’explorateur de solutions, effectuez un clic droit sur Content et choisissez
Ajouter puis Élément existant (figure 3-4).

Figure 3-4
Ajout d’un objet au Content Manager

2. Allez chercher le fichier désiré puis validez.


3. Sélectionnez-le ensuite dans l’explorateur de solutions puis visualisez ses propriétés
(raccourci clavier F4).

Figure 3-5
Propriétés de notre texture

Le champ Asset Name (figure 3-5) contient le nom que vous devrez spécifier pour utiliser
la texture dans votre jeu. Par défaut, il s’agit du nom du fichier sans son extension. Vous
pouvez également définir la manière dont sera traité le fichier par les champs Content
Importer et Content Processor, mais vous n’y toucherez pas pour le moment.

Fichiers XNB
Lors de la compilation du projet, le Content Manager transformera notre fichier PNG en un fichier XNB
(optimisé afin d’améliorer sa vitesse de chargement lors de l’exécution du jeu) qu’il placera dans le répertoire
du jeu, le champ Copier dans le répertoire de sortie concerne le fichier original et non le fichier .xnb, nous
conserverons donc l’option Ne pas copier.
=Labat FM.book Page 39 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
39

Il faut à présent stocker notre image dans un objet de type Texture2D. Pour cela, déclarons-
le au début de notre classe :
Texture2D balleTexture;
Puis, dans la méthode LoadContent(), chargeons notre image :
balleTexture = Content.Load<Texture2D>("balle"); 

Syntaxe
Content.Load<T>(string assetName);
T : Type de l’objet à charger
assetName : Nom de l’objet à charger

Nous allons maintenant définir les coordonnées X et Y de notre image. Pour cela, nous
disposons de la structure Vector2. Ajoutons un vecteur au début de notre classe :
Vector2 ballePosition;
Dans ce premier essai, nous allons placer notre sprite à 10 pixels du bord gauche et 20 pixels
du bord supérieur de l’écran. Définissons donc notre vecteur dans la méthode Initialize()
de notre classe :
ballePosition = new Vector2(10, 20);

Syntaxe
La structure Vector2 dispose de plusieurs constructeurs :
Vector2()
Vector2(float value)
Vector2(float x, float y)

Langage C#
En C#, il est possible de définir plusieurs méthodes du même nom proposant des arguments variables en
nombre et en type. Ce principe s’appelle la surcharge de méthode.
Notez qu’il est également possible de surcharger les opérateurs (+, -, *, etc.) en redéfinissant leur signifi-
cation pour une classe.

L’affichage à l’écran de notre sprite se fait très facilement grâce à notre objet SpriteBatch
et sa méthode Draw(). Cela dit, avant tout appel à cette méthode, il faut préparer les méca-
nismes de dessin qui devront être utilisés grâce à la méthode Begin(). Après la méthode
Draw(), il faudra également appeler la méthode End() qui sérialisera les informations de
rendu vers la carte graphique.
spriteBatch.Begin();
spriteBatch.Draw(balleTexture, ballePosition, Color.White);
spriteBatch.End();
=Labat FM.book Page 40 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


40

Modulation
La couleur que nous passons à la méthode Draw() sert à moduler les couleurs de notre sprite. Le fait de
passer une couleur blanche équivaut à ne pas utiliser de modulation.

Syntaxe
La méthode Draw() possède de nombreuses surcharges que nous étudierons au fur et à mesure de nos
besoins. Voici celle que nous venons de rencontrer :
public void Draw (Texture2D texture, Vector2 position, Color color)
texture : la texture du sprite
position : la position du sprite à l’écran
color : la couleur à utiliser pour la modulation

Appuyez ensuite sur la touche F5 pour lancer la compilation et démarrer le jeu (figure 3-6).

Figure 3-6
Affichage de notre premier sprite

Afficher plusieurs sprites


Imaginons qu’à présent nous souhaitions afficher plusieurs balles à l’écran. Nous avons
écrit au début de ce chapitre que, pour des questions d’optimisation, on ne charge qu’une
seule fois chaque image et on y connecte les sprites qui l’utilisent.
Pour mettre ce principe en œuvre, il suffit d’ajouter les composants manquants à l’affi-
chage de notre second sprite, c’est-à-dire un second vecteur position.
Vector2 ballePosition2;
Définissons maintenant les coordonnées de ce vecteur. Nous voulons que cette deuxième
balle vienne se placer au milieu de l’écran.
=Labat FM.book Page 41 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
41

On peut récupérer la largeur et la hauteur de l’écran grâce à l’objet graphics. Pour centrer
l’objet, il faudra diviser ces dimensions par deux, mais attention, l’origine du sprite n’est
pas placée en son centre, mais sur son coin supérieur gauche. Ainsi, pour le centrer tota-
lement à l’écran, il faut encore lui retirer la moitié de sa largeur et de sa hauteur, soit :
ballePosition2 = new Vector2(graphics.PreferredBackBufferWidth /
➥ 2 - balleTexture.Width / 2, graphics.PreferredBackBufferHeight /
➥ 2 - balleTexture.Height / 2);

Attention
Nous ne pouvons pas définir cette position dans la méthode Initialize(). En effet, nous utilisons la taille
de balleTexture, or le chargement de balleTexture s’effectue dans la méthode LoadContent(),
c’est-à-dire après la méthode Initialize(). Nous devons donc définir notre vecteur juste après le
chargement de l’image.

Il ne reste plus qu’à le dessiner à l’écran (figure 3-7) :


spriteBatch.Draw(balleTexture, ballePosition2, Color.White);

Figure 3-7
Affichage d’un second sprite

Si vous aviez voulu afficher autre chose qu’une seconde balle, vous auriez bien évidemment
dû charger une seconde texture.

Un sprite en mouvement
Votre objectif à présent est de donner du mouvement à votre premier sprite : vous allez le
faire rebondir sur les bords de l’écran. Pour cela, vous devez recalculer sa position à
=Labat FM.book Page 42 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


42

chaque appel de la méthode Update(). Pour commencer, vous modifierez cette position de
+1 pixel sur l’axe des abscisses à chaque itération.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

ballePosition.X += 1;

base.Update(gameTime);
}
En démarrant le jeu, vous remarquez que votre balle se déplace vers la droite de l’écran…
Malheureusement, elle sort de celui-ci. Pour la faire rebondir, vous devrez d’abord ajouter
la notion de direction. Déclarez un objet Vector2 au début de votre classe :
Vector2 balleDirection;

Figure 3-8
Principe de direction de
notre sprite à l’écran

Dans la méthode Initialize(), définissez la direction de départ. Par exemple, vers la


gauche et vers le bas (figure 3-8) :
balleDirection = new Vector2(-1, 1);
Puis à chaque appel à Update(), il faut déplacer votre sprite dans la direction voulue.
Additionnez donc les deux vecteurs :
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

ballePosition += balleDirection;

base.Update(gameTime);
}
Votre sprite se déplace à présent bien vers le bas et vers la gauche, mais il sort toujours de
l’écran. Il faut ajouter dans la méthode Update(), juste avant de calculer sa nouvelle posi-
tion, des conditions qui vont tester s’il sort de l’écran. Si, durant son mouvement, il
rencontre un bord de l’écran, inversez tout simplement la direction de l’axe concerné.
=Labat FM.book Page 43 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
43

Attention, la position de votre sprite est déterminée par son coin supérieur gauche. Pour
tester s’il sort en bas ou à droite de l’écran, il va donc falloir lui retrancher respectivement
sa hauteur ou sa largeur.
if (ballePosition.X <= 0)
balleDirection.X *= -1;
if (ballePosition.Y <= 0)
balleDirection.Y *= -1;
if (ballePosition.X >= graphics.PreferredBackBufferWidth - balleTexture.Width)
balleDirection.X *= -1;
if (ballePosition.Y >= graphics.PreferredBackBufferHeight - balleTexture.Height)
balleDirection.Y *= -1;
Il reste une dernière notion à implanter : la vitesse. Ajoutez une variable de type float à
votre classe :
float vitesse;
Dans la méthode Initialize(), donnez lui une valeur :
vitesse = 0.2f;
Avant de prendre en compte cette vitesse dans le calcul de la nouvelle position de la balle,
intéressez-vous à un dernier problème : comment s’assurer que la balle se déplacera à la
même vitesse sur tous les supports où votre jeu tournera ?
Admettons que nous ayons défini un déplacement de 3 pixels à chaque appel de la
méthode Update(). Vous disposez de deux PC : sur le PC A, il s’écoule 1 ms entre chaque
appel de la méthode Update() et 0,5 ms sur le PC B.
Ainsi, en 1 ms, le PC A aura appelé la méthode une fois et sa balle se sera déplacée de
3 pixels, tandis que le PC B aura appelé la méthode deux fois, sa balle s’étant alors déplacée
de 6 pixels (figure 3-9).

Figure 3-9
Deux PC ne mettent pas toujours le même temps à effectuer les mêmes calculs

Si vous régulez la vitesse en fonction du temps écoulé entre chaque appel à Update() (en
ms), vous obtenez les calculs suivants :
Vpca = 3 ¥ 1 = 3 pixels
Vpcb = 3 ¥ 0,5 = 1,5 pixels
En 1 ms, la méthode Update() sera appelée deux fois sur le PC B, mais la balle ne se sera
déplacée que de 1,5 pixels à chaque appel ; au final, elle aura donc autant avancé que
celle du PC A.
=Labat FM.book Page 44 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


44

Heureusement, la méthode Update() reçoit un objet GameTime comme paramètre. Grâce à


ce dernier, vous avez accès au nombre de millisecondes écoulées depuis le dernier appel
à Update.
Au niveau du code, voici ce que cela donne :
ballePosition += (balleDirection * vitesse * gameTime.ElapsedGameTime.Milliseconds);
Voici le code source récapitulant les points vus précédemment :
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace Chap3
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D balleTexture;
Vector2 ballePosition;
Vector2 ballePosition2;
Vector2 balleDirection;
float vitesse;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
base.Initialize();
ballePosition = new Vector2(10, 20);
balleDirection = new Vector2(-1, 1);
vitesse = 0.2f;
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
balleTexture = Content.Load<Texture2D>("balle");
ballePosition2 = new Vector2(graphics.PreferredBackBufferWidth /
➥ 2 - balleTexture.Width / 2
, graphics.PreferredBackBufferHeight / 2 - balleTexture.Height / 2);
}
=Labat FM.book Page 45 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
45

protected override void UnloadContent()


{
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
➥ ButtonState.Pressed)
this.Exit();
if (ballePosition.X <= 0)
balleDirection.X *= -1;
if (ballePosition.Y <= 0)
balleDirection.Y *= -1;
if (ballePosition.X >= graphics.PreferredBackBufferWidth -
➥ balleTexture.Width)
balleDirection.X *= -1;
if (ballePosition.Y >= graphics.PreferredBackBufferHeight -
➥ balleTexture.Height)
balleDirection.Y *= -1;

ballePosition += (balleDirection * vitesse *


➥ gameTime.ElapsedGameTime.Milliseconds);

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
graphics.GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
spriteBatch.Draw(balleTexture, ballePosition, Color.White);
spriteBatch.Draw(balleTexture, ballePosition2, Color.White);
spriteBatch.End();

base.Draw(gameTime);
}
}
}

Une classe pour gérer vos sprites


Pour vous faciliter la tâche lors de vos développements futurs, vous allez maintenant
écrire une classe pour créer facilement des sprites. Vous continuerez à améliorer et à utiliser
cette classe tout au long de ce livre.

Créer une classe Sprite


Ajoutez une nouvelle classe à votre projet et nommez-la « Sprite ».
=Labat FM.book Page 46 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


46

Comme vous l’avez vu au début de ce chapitre, un sprite a besoin d’une position et d’une
texture. Commencez donc par ajouter ces deux champs à votre classe :
Vector2 position;
Texture2D texture;
La position d’un sprite doit être définie dès le début, c’est-à-dire dans le constructeur de
la classe. Surchargez-le pour qu’il accepte des coordonnées sous forme de Vector2 ou de
simples nombres réels.
public Sprite(Vector2 position)
{
this.position = position;
}
public Sprite(float x, float y)
{
position = new Vector2(x, y);
}
La méthode qui suit est utile au chargement de la texture utilisée par votre sprite.
public void LoadContent(ContentManager content, string assetName)
{
texture = content.Load<Texture2D>(assetName);
}
Les deux méthodes suivantes seront celles appelées à chaque frame (image) du jeu. Notez
qu’elles portent le même nom que celles de la structure de base de votre code.
À vrai dire, la méthode Update aurait pu ne pas être implantée et les traitements s’effectuer
directement sur le champ position via une propriété. Là encore, c’est une décision
personnelle et le développeur est totalement libre de choisir la solution qu’il juge la plus
adaptée.
public void Update(Vector2 translation)
{
position += translation;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, Color.White);
}
Enfin, voici le code source final de la classe :
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace PremierProjetXNA
{
class Sprite
=Labat FM.book Page 47 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
47

{
Vector2 position;
Texture2D texture;

public Sprite(Vector2 position)


{
this.position = position;
}

public Sprite(float x, float y)


{
position = new Vector2(x, y);
}

public void LoadContent(ContentManager content, string assetName)


{
texture = content.Load<Texture2D>(assetName);
}

public void Update(Vector2 translation)


{
position += translation;
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, Color.White);
}
}
}
Vous serez amené à étoffer cette classe dès la fin de ce chapitre.

Utiliser la classe Sprite


Votre classe étant prête, vous allez maintenant apprendre à l’utiliser.
Retournez dans votre fichier Game1.cs et ajoutez un champ correspondant à un personnage
que vous voulez afficher à l’écran.
Sprite personnage;
Évidemment, l’initialisation de la variable se fait dans la méthode Initialize.
personnage = new Sprite(100, 100);
Le constructeur utilisé ici est celui qui attend deux nombres réels. La ligne suivante
utilise le constructeur qui attend un vecteur.
personnage = new Sprite(new Vector2(100, 100));
Libre à vous de choisir celui que vous préférez !
=Labat FM.book Page 48 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


48

Dans la méthode LoadContent, comme son nom l’indique, il faut charger l’image pour le
sprite.
personnage.LoadContent(Content, "personnage");
Dans la méthode Update, vous devez simplement passer le vecteur utile à la translation du
personnage.
personnage.Update(new Vector2(gameTime.ElapsedGameTime.Milliseconds * speed,
➥ gameTime.ElapsedGameTime.Milliseconds * speed));
Et, enfin, dans la méthode Draw, il suffit de dessiner le personnage.
personnage.Draw(spriteBatch);

Figure 3-10
L’utilisation d’une classe simplifie l’affichage d’un sprite

Votre code est maintenant plus clair et l’utilisation de sprite est simplifiée ! Vous retrou-
verez ci-dessous le code source de l’utilisation de votre classe Sprite (figure 3-10).
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
=Labat FM.book Page 49 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
49

using Microsoft.Xna.Framework.Storage;
namespace PremierProjetXNA
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Sprite personnage;
float speed;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
speed = 0.2f;
}
protected override void Initialize()
{
personnage = new Sprite(100, 100);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
personnage.LoadContent(Content, "personnage");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
➥ ButtonState.Pressed)
this.Exit();
personnage.Update(new Vector2(gameTime.ElapsedGameTime.Milliseconds *
➥ speed, gameTime.ElapsedGameTime.Milliseconds * speed));
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
=Labat FM.book Page 50 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


50

personnage.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
}

Classe héritée de Sprite


Il est temps d’illustrer une nouvelle facette de la programmation objet en s’intéressant au
cas d’un mini RPG (Role Playing Game, jeu de rôle).
Dans votre projet, ajoutez une nouvelle classe que vous baptiserez Human. Ici, l’objectif
est de créer un personnage qui sera représenté à l’écran par un sprite pourvu de caracté-
ristiques spécifiques, telles qu’un nom, un niveau d’intelligence, etc. Ainsi, il est possible
de considérer un objet Human comme un objet Sprite davantage spécialisé. Notez que la
même réflexion est tout à fait possible avec une automobile ou un ballon de football : il
s’agit d’entités qui possèdent les caractéristiques d’un objet Sprite, mais pas seulement.
Dans le monde de la programmation objet, cette notion s’appelle l’héritage.

Role Playing Game


Le gameplay des jeux de rôle tels que Oblivion utilise des systèmes de classes et de spécialisations,
chaque classe ou spécialisation ayant des traits communs mais aussi des traits plus particuliers. L’héritage
est donc tout à fait approprié pour modéliser ce comportement.

En C#, la déclaration d’une classe qui hérite d’une autre se fait de la manière suivante :
class Fille: Mere
{
}
Dans le cas étudié, il s’agit donc de :
class Human: Sprite
{
}

Héritage multiple
Si vous connaissez le langage C++, vous avez sûrement entendu parler d’héritage multiple, bien qu’il ne
soit que peu conseillé. En C#, cette notion n’existe pas. Cependant, l’utilisation d’interfaces constitue une
autre approche du problème. Ce mécanisme du langage sera expliqué dans le chapitre 4.

Si, à ce stade, vous essayez de compiler le projet, vous obtiendrez l’erreur suivante :
'PremierProjetXNA.Sprite' ne contient pas un constructeur qui accepte des arguments
➥ '0'
=Labat FM.book Page 51 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
51

En effet, vous n’avez pas encore implanté le constructeur de votre classe. À cela, vous
devrez ajouter un appel au constructeur de la classe mère :
public Human(Vector2 position)
: base(position)
{
}
Vous pouvez dès maintenant utiliser votre classe Human plutôt que la classe Sprite. Retournez
dans le fichier Game1.cs et modifiez le type de personnage. Le code compilé, le programme
fait exactement la même chose qu’avant. Cependant vous remarquez que, dans le fichier
Game1.cs, vous utilisez des méthodes de l’objet personnage qui ne sont pourtant pas définies
dans la classe Human. C’est tout l’intérêt de l’héritage : la réutilisabilité du code.
Il est temps d’ajouter certains attributs à cette nouvelle classe, notamment un nom et des
points de vie.
string name;
int health;
Vous allez modifier le constructeur de la classe de la manière suivante :
public Human(Vector2 position, string name, int health)
: base(position)
{
this.name = name;
this.health = health;
}
Ainsi que son appel dans le fichier Game1.cs :
personnage = new Human(new Vector2(100,100), "Heros", 100);
Le constructeur peut très bien prendre un plus grand nombre de paramètres que le construc-
teur de la classe mère. Notez que vous êtes maintenant en mesure de comprendre le code
de base d’une application utilisant XNA. Regardez le code de la classe Game1. Celle-ci est
dérivée de la classe Game. Analysez la fonction suivante :
protected override void Initialize()
{
personnage = new Human(new Vector2(100,100), "Heros", 100);
base.Initialize();
}
Le mot-clé override dans la déclaration de la fonction signifie que vous souhaitez rempla-
cer la définition de la fonction dans la classe mère par celle spécifiée dans la classe fille,
c’est-à-dire que vous redéfinissez la fonction. La ligne base.Initialize(); signifie, quant
à elle, que vous appelez la fonction telle qu’elle a été définie dans la classe mère.
Vous retrouvez ci-dessous le code source de l’exemple que vous venez de traiter :
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
=Labat FM.book Page 52 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


52

namespace PremierProjetXNA
{
class Human : Sprite
{
string name;
int health;
public Human(Vector2 position, string name, int health)
 : base(position)
{
this.name = name;
this.health = health;
}
}
}
Qu’ont un mage et un guerrier en commun ? À l’origine il s’agit bien évidemment
d’humains… Encore que tout amateur de jeux de rôles vous dira qu’un orque ou encore
un elfe peut tout aussi bien accomplir cette tâche. Là encore, la notion d’héritage peut
s’appliquer…
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace PremierProjetXNA
{
class Wizard : Human
{
int intelligence;
public Wizard(Vector2 position, string name, int health, int intelligence)
 : base(position, name, health)
{
this.intelligence = intelligence;
}
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace PremierProjetXNA
{
class Warrior : Human
{
int strength;
public Warrior(Vector2 position, string name, int health, int strength)
 : base(position, name, health)
{
this.strength = strength;
}
}
}
=Labat FM.book Page 53 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
53

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PremierProjetXNA
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Wizard mage;
Warrior guerrier;
float speed;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
speed = 0.2f;
}
protected override void Initialize()
{
mage = new Wizard(new Vector2(100,100), "Gandalf", 100, 98);
guerrier = new Warrior(new Vector2(300, 100), "Conan", 150, 100);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
mage.LoadContent(Content, "personnage");
guerrier.LoadContent(Content, "personnage");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
➥ ButtonState.Pressed)
this.Exit();
mage.Update(new Vector2(gameTime.ElapsedGameTime.Milliseconds * speed,
➥ gameTime.ElapsedGameTime.Milliseconds * speed));
guerrier.Update(new Vector2(0, 0));
base.Update(gameTime);
}
=Labat FM.book Page 54 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


54

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
mage.Draw(spriteBatch);
guerrier.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Vous pouvez ainsi imaginer une hiérarchie de classes : celles se trouvant en haut de la
pyramide sont généralistes et plus l’on descend, plus elles sont spécialisées (figure 3-11).
Figure 3-11
Un diagramme de classes
représentant notre exemple

Imaginez toujours un monde fantastique, dans lequel personne n’est ordinaire ! Il est
donc possible de considérer que tous les habitants de ce monde sont des mages ou des
guerriers. Ainsi, la classe Human ne devrait que servir de base aux autres classes et ne plus
être directement instanciable. Ce concept se traduit en C# par le mot clé abstract.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace PremierProjetXNA
{
abstract class Human : Sprite
{
string name;
int health;
public Human(Vector2 position, string name, int health)
 : base(position)
{
this.name = name;
this.health = health;
}
}
}
=Labat FM.book Page 55 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
55

Un gestionnaire d’images
Le composant que vous allez maintenant développer a pour but de fournir des textures à
la demande. Vous souhaitez pouvoir charger une texture la première fois que le jeu la
demande, puis la stocker dans une collection. Ensuite, dès que le jeu la requiert une
nouvelle fois, il faudra lui passer celle qui se situe déjà en mémoire.
Cependant, depuis la sortie d’XNA 2.0, ce composant n’a plus de raison d’être. En effet,
le mécanisme est maintenant géré par le Content Manager. Le gestionnaire ne vous sera
donc d’aucune utilité dans vos futurs projets, toutefois son écriture vous fera découvrir de
nouveaux concepts clés du langage et vous fera pratiquer ceux que vous venez de découvrir.
Essayer de reproduire les fonctionnalités d’une solution existante telle que le Content
Manager est aussi un excellent moyen pour vraiment en comprendre le fonctionnement.
Avant de vous lancer directement dans l’écriture du gestionnaire, il reste des notions du
langage que vous devez acquérir. Pour leur apprentissage, veuillez sauvegarder et fermer
votre premier projet XNA et en créer un nouveau en mode console.

Les boucles en C#
Une boucle sert à répéter une action en fonction d’une condition. Il existe plusieurs types
de boucles. Le premier type de boucle est while, qui permet de répéter une action tant que
la condition reste vraie. Voici la structure algorithme correspondante :
Tant que condition vraie
Faire
Fin tant que
En C# :
int i = 0;
while (i < 5)
{
Console.WriteLine("Hello");
i++;
}
Le deuxième type est la boucle do… while : le programme exécute automatiquement au
moins une fois le code contenu dans la boucle, même si la condition est fausse.
Faire
Tant Que Condition Vraie
En C# :
int i = 0;
do
{
Console.WriteLine("Hello");
i++;
} while (i < 5) ;
=Labat FM.book Page 56 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


56

Pour terminer, la boucle for est équivalente à la boucle while. Il s’agit seulement d’une
forme plus condensée de son écriture. Voici l’écriture algorithmique qui lui correspond :
Pour i de n a m avec un pas de k
Faire
Fin pour
En C# :
for (int i = 0; i < 5; i++)
Console.WriteLine("Hello");
Bien évidemment, si le code à executer tient sur plusieurs lignes, vous devrez écrire les
accolades.

Attention
Les boucles infinies peuvent causer l’instabilité d’un système, notamment lorsque la condition reste toujours
vraie. Faites donc toujours bien attention lorsque vous établissez des conditions.

Il existe deux dernières instructions particulières pour la gestion de vos boucles.


L’instruction break permet d’en sortir.
for (int i = 0; i < 10; i++)
{
if (i == 5)
break;
Console.WriteLine(i);
}
Ce qui nous donne dans la console :

0
1
2
3
4

À l’inverse, l’instruction continue fait passer directement à l’itération suivante.


for (int i = 0; i < 3; i++)
{
if (i == 1)
continue;
Console.WriteLine(i);
}
Le résultat dans la console est :

0
2
=Labat FM.book Page 57 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
57

Les tableaux
Les tableaux rassemblent un ensemble de variables du même type. En C#, un tableau
d’entiers se déclare de la façon suivante :
int[] tableau = new int[3];
Ou bien, d’une manière plus générale :
Type[] tableau[]=new Type[n];
Où le nombre n correspond à la taille du tableau.
Comme pour les variables, l’initialisation d’un tableau peut se faire en même temps que
sa déclaration. Deux syntaxes sont disponibles :
int[] tableau = new int[] { 10, 20, 30 };
int[] tableau = { 10, 20, 30 };
La lecture d’un élément du tableau se fait de la manière suivante :
Console.WriteLine(tableau[2]);

En pratique
Le premier élément d’un tableau a l’indice 0. Ainsi, dans le tableau déclaré ci-dessus, l’élément 3 n’existe
pas et, lorsque vous affichez l’élément 2, c’est le nombre 30 qui s’affiche dans la console.

Il est également possible de déclarer des tableaux à deux dimensions.


int[,] tableau = { { 1, 2, 3 }, { 4, 5, 6 } };
Console.WriteLine(tableau[1,2]);
Pour vous familiariser avec l’utilisation des boucles, entraînez-vous à parcourir tous les
éléments d’un tableau. Pour vous aider, les tableaux possèdent des propriétés permettant
de connaître leur taille. Ainsi, dans le cas d’un tableau à une dimension, vous pouvez
utiliser la propriété Length et écrire :
for (int i = 0; i < tableau.Length; i++ )
{
Console.WriteLine(tableau[i]);
}
Si vous utilisez des tableaux multi-dimensionnels, vous accédez à la taille de chaque
dimension via la méthode GetLength(i), où i est l’indice de la dimension concernée.
int[,] tableau = {{1, 2, 3},{4,5,6}};
for (int i = 0; i < tableau.GetLength(0); i++ )
{
string output = "";
for (int j = 0; j < tableau.GetLength(1); j++)
{
if (j == 0)
output += tableau[i,j].ToString();
=Labat FM.book Page 58 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


58

else
output += ";" + tableau[i,j].ToString();
}
Console.WriteLine(output);
}
Cet exemple retourne dans la console :

1;2;3
4;5;6

Il existe un dernier type de boucle que nous n’avons pas encore mentionné. Il s’agit de la
boucle foreach. Sa syntaxe est la suivante :
foreach (Type variable in Collection)
{
// Traitements
}
Une collection est un ensemble d’objets énumérable. Vous venez d’en découvrir une : les
tableaux. Ainsi, vous pouvez également parcourir un tableau unidimensionnel d’entiers
de la manière suivante :
int[] tableau = {1, 2, 3};
foreach (int entier in tableau)
Console.WriteLine(entier);
Les chaînes de caractères sont également énumérables :
string chaine = "salut";
foreach (char c in chaine)
Console.WriteLine(c);

Les collections
Le framework .NET fournit des classes utiles pour le stockage de données. Chacune a
une particularité dans sa manière de stocker des éléments. En fonction de vos besoins, vous
serez donc amené à choisir telle ou telle classe, ou même à créer la vôtre, qui hériterait
des particularités d’une des collections de base du framework.
Les deux classes dont nous allons parler sont des collections génériques, c’est-à-dire qu’elles
permettent de stocker n’importe quel type de données. Elles se trouvent dans l’espace de
noms System.Collections.Generic. Si elles n’apparaissent pas, ajoutez la directive using.
Commençons tout d’abord par la classe List<T>. Elle vous permet de stocker des objets
de types T (remplacez T par le type que vous voulez, d’où le nom Generic, générique en
français) et se manipule de la même manière qu’un tableau, sauf que vous n’êtes pas
obligé de lui donner une taille, c’est-à-dire qu’elle est autonome dans la gestion de sa
capacité.
List<int> liste = new List<int>();
=Labat FM.book Page 59 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
59

Vous pouvez toutefois lui indiquer sa capacité de stockage dès son initialisation.
List<int> liste = new List<int>(5);
Une dernière surcharge du constructeur vous permet enfin d’initialiser une liste en copiant
les valeurs d’une autre liste.
List<int> premiereListe = new List<int>();
List<int> liste = new List<int>(premiereListe);
Pour ajouter un élément à la liste, il suffit simplement d’écrire :
List<int> liste = new List<int>();
liste.Add(5);
L’accès à l’élément se fait exactement de la même manière que dans un tableau :
Console.WriteLine(liste[0]);
Vous pouvez récupérer le nombre d’éléments de la liste via la propriété Count.
Console.WriteLine(liste.Count);
Enfin, la liste dispose d’un très grand nombre de méthodes, dont voici un extrait :

Méthode Description
Public void Clear() Supprime tous les éléments de la liste.

Public bool Contains(T item) Renvoie true si l’élément spécifié est dans la liste.

Public bool Remove(T item) Supprime item de la liste. Renvoie true si l’opération s’est
déroulée avec succès.

La classe Dictionary<TKey,TValue> porte bien son nom : une clé est liée à une valeur, de la
même façon que dans un dictionnaire une définition est liée à un mot. Il est donc possible
de représenter cette collection comme un tableau :

Clé Valeur
keyA valueA
keyB valueB

La déclaration d’un dictionary se fait de la façon suivante :


Dictionary<string, int> dico = new Dictionary<string, int>();
Dans cet exemple, les clés correspondent donc à des chaînes de caractères et les valeurs
à des entiers. Pour ajouter une entrée dans le dictionnaire, il suffit de procéder ainsi :
dico.Add("premiere", 12);

Attention
Dans une collection de ce type, chaque clé doit être unique.
=Labat FM.book Page 60 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


60

On accède ensuite à une valeur en utilisant la clé à laquelle elle est liée :
Console.WriteLine(dico["premiere"]);

Tout comme pour les listes, il existe de nombreuses méthodes liées à cette classe. Reportez-
vous à la documentation du framework si vous souhaitez plus de détails (http://msdn.microsoft
.com/en-us/netframework/default.aspx).
Il existe bien sûr beaucoup d’autres classes pour le stockage de données qui sont fournies
par le framework .NET, mais ce n’est pas le but de cet ouvrage que de les lister toutes
et de les étudier en détail. Pour les découvrir, parcourez les espaces de noms System
.Collections et System.Collections.Generic.

Écriture du gestionnaire d’images


Avant de programmer le gestionnaire, remémorez-vous l’idée qui le sous-tend : votre
programme utilise des sprites, qui peuvent avoir à utiliser la même image. Vous voulez
donc vous assurer que chaque image ne sera chargée qu’une seule fois en mémoire et sera
partagée entre les sprites qui doivent l’utiliser.

Figure 3-12
Diagramme de séquence correspondant à notre objectif

Créez un nouveau projet sous XNA et ajoutez-lui la classe Sprite que vous avez codée plus
tôt dans ce chapitre (récrivez-la ou allez chercher le fichier via l’explorateur de solutions,
au choix).
=Labat FM.book Page 61 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
61

Si le Content Manager n’accomplissait pas déjà cette fonction, le gestionnaire aurait un


réel intérêt dans les situations où il existe un grand nombre de sprites qui emploient la
même image. Commencez donc par ajouter au projet l’image que vous chargerez. Le
fichier GameThumbnail.png présent dans chaque projet peut très bien faire l’affaire.
Dans la classe Game1, ajoutez une liste de sprites et, dans la méthode Initialize(), remplissez
la liste de manière à recouvrir l’écran de sprites.
List<Sprite> sprites = new List<Sprite>();

protected override void Initialize()


{
for (int i = 0; i < 13; i++)
{
for (int j = 0; j < 10; j++)
{
sprites.Add(new Sprite(i * 64, j * 64));
}
}
base.Initialize();
}
Notez que, dans cet exemple, les nombres 64 correspondent aux dimensions de l’image
GameThumbnail.png. Si vous choisissez une autre image, n’oubliez pas de modifier ces
dimensions.
Chargez ensuite normalement vos images dans la méthode LoadContent(). Remarquez
encore une fois la facilité déconcertante de la programmation avec XNA et C#.
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);

foreach (Sprite sprite in sprites)


sprite.LoadContent(Content, "GameThumbnail");
}
Enfin, il ne reste plus qu’à dessiner les sprites de la collection.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
foreach (Sprite sprite in sprites)
sprite.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
=Labat FM.book Page 62 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


62

À présent, ajoutez une nouvelle classe au projet et nommez-la TextureManager. Votre


gestionnaire devra utiliser une collection pour stocker les images. En analysant le
problème, on se rend rapidement compte qu’un dictionnaire est tout à fait approprié : lier
le nom d’une image à l’image elle-même est une bonne solution.
Ajoutez également un objet de type ContentManager que vous référencerez dans le construc-
teur de la classe. Ainsi, vous n’aurez pas à le repasser en argument à chaque utilisation du
gestionnaire.
Il ne reste plus que la méthode qui nous intéresse tout particulièrement. La figure 3-12
résume bien le comportement que vous devez programmer. Utilisez la méthode ContainsKey()
du dictionnaire pour vérifier si une image a déjà été chargée ; si c’est le cas, renvoyez-la,
sinon chargez-la avant de la renvoyer.
using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace GestionnaireImage
{
class TextureManager
{
Dictionary<string, Texture2D> textureBank = new Dictionary<string,
➥ Texture2D>();
ContentManager content;
public TextureManager(ContentManager content)
{
this.content = content;
}
public Texture2D GetTexture(string assetName)
{
if (textureBank.ContainsKey(assetName))
return textureBank[assetName];
else
{
textureBank.Add(assetName, content.Load<Texture2D>(assetName));
return textureBank[assetName];
}
}
}
}
Il ne reste plus qu’à utiliser le gestionnaire. Dans la définition de la classe Sprite, modifiez
la méthode LoadContent(). À la place du paramètre de type ContentManager, ajoutez-en un
de type TextureManager et employez la fonction que vous venez de définir.
public void LoadContent(TextureManager textureManager, string assetName)
{
texture = textureManager.GetTexture(assetName);
}
=Labat FM.book Page 63 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
63

Pour terminer, dans la classe Game1, déclarez votre TextureManager, initialisez-le et modifiez
l’appel à LoadContent() de vos sprites.
TextureManager textureManager;

protected override void Initialize()


{
textureManager = new TextureManager(Content);
// …
}

protected override void LoadContent()


{
// …

foreach (Sprite sprite in sprites)


sprite.LoadContent(textureManager, "GameThumbnail");
}
Vous pouvez à présent démarrer le programme. La version qui utilise le gestionnaire a le
même comportement que celle qui ne l’utilise pas. Cependant, il s’agit là d’un bon
entraînement aux collections et aux boucles et c’est aussi une bonne introduction à la
mesure des performances de votre code.

Mesure des performances


Il existe deux principaux facteurs de performances que vous pouvez mesurer : l’utilisation
mémoire et le temps d’exécution.

Figure 3-13
Utilisation mémoire (les deux courbes se chevauchent)
=Labat FM.book Page 64 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


64

Dans le cas du gestionnaire d’images, essayez donc d’exécuter le programme tout en


jetant un œil à l’onglet Processus du gestionnaire des tâches de Windows, et tout particu-
lièrement à la colonne Utilisation mémoire. Effectuez la même expérience avec un nombre
de sprites identique, en employant la même image, mais sans utiliser le gestionnaire
d’images. Vous observerez que la mémoire employée dans le premier cas est légèrement
supérieure à celle utilisée dans le second cas.
La mesure du temps d’exécution peut se faire avec une précision de l’ordre du millième de
seconde grâce à un objet Stopwatch, qui se trouve dans l’espace de noms System.Diagnostics.
Ajoutez un objet de ce type au projet, démarrez le chronomètre avant le chargement des
images et arrêtez-le juste après. Enfin, affichez le résultat dans le titre de la fenêtre grâce
à la propriété Title de l’objet Window.
Stopwatch stopWatch = new Stopwatch();

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);

stopWatch.Start();
foreach (Sprite sprite in sprites)
sprite.LoadContent(textureManager, "GameThumbnail");
stopWatch.Stop();
}

protected override void Update(GameTime gameTime)


{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

this.Window.Title = stopWatch.ElapsedMilliseconds.ToString();

base.Update(gameTime);
}

Charge du processeur
Le nombre de sprites a volontairement été augmenté, de manière à souligner les variations de performan-
ces entre les différentes solutions techniques.

Faites ensuite la même chose pour la version du programme qui n’utilise pas le gestion-
naire. La figure 3-14 compare les résultats entre les deux versions. À chaque fois,
plusieurs lancements de l’application ont été effectués pour ne garder que la moyenne des
valeurs obtenues. Cette fois-ci, vous constatez que la version qui emploie le gestionnaire
d’images est plus performante.
=Labat FM.book Page 65 Vendredi, 19. juin 2009 4:01 16

Afficher et animer des images : les sprites


CHAPITRE 3
65

Figure 3-14
Temps d’exécution (en ms) avec et sans gestionnaire d’images

Cependant, considérez bien ces résultats. La différence la plus significative est de l’ordre
de quelques centaines de millisecondes et a lieu lors du chargement des sprites… Qui
plus est, ce nombre de sprites est assez élevé (plus d’un million) et le chargement d’une
telle scène est somme toute assez rare. Au final, l’utilisation du gestionnaire n’aura que
très peu d’impact sur votre jeu, cependant, en le modifiant légèrement, il pourrait par
exemple vous servir de bibliothèque de texture pour un éditeur de carte.
Essayez toujours, à tout moment du développement de vos jeux, de trouver la solution la
plus performante pour chaque mécanisme. Le temps que vous consacrez à ces optimisa-
tions n’est jamais perdu et peut vite se ressentir dans l’expérience d’utilisation de votre
jeu, ou bien, dans le cas présent, vous fera découvrir le fonctionnement interne du Content
Manager.

En résumé
Dans ce chapitre, vous avez découvert :
• ce qu’est un sprite, comment l’afficher et le faire se déplacer à l’écran ;
• comment créer une classe (la classe Sprite) et l’utiliser dans un exemple concret avec
XNA ;
• des notions du langage C# (l’héritage, les boucles, les tableaux et les collections) ;
• comment faire pour avoir une idée générale des performances de votre jeu.
=Labat FM.book Page 66 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 67 Vendredi, 19. juin 2009 4:01 16

4
Interactions avec le joueur

Dans un jeu vidéo, l’interaction avec le joueur peut se faire via différents périphériques.
Ce chapitre a pour but de vous apprendre à contrôler les périphériques utilisables avec
XNA. Ce sera aussi l’occasion de découvrir un nouvel aspect de la programmation objet
que vous appliquerez directement pour la gestion du clavier. Nous verrons également des
exemples d’interactions avancées intéressantes à mettre en place dans vos jeux.

Utiliser les périphériques


Cette première partie va vous présenter les différents périphériques compatibles avec
XNA et leur utilisation. N’oubliez pas que, pour la Xbox 360, la souris et le clavier ne
constituent pas des périphériques de base, vous devrez alors plutôt vous concentrer sur la
gestion de la manette. En revanche, la situation est inversée si vous développez pour
Windows. Toutefois, vous allez constater que les périphériques se gèrent très facilement
avec XNA. Le portage de votre code d’une machine vers une autre est, de ce fait, un jeu
d’enfant !

Le clavier
Le clavier est un périphérique de base pour un jeu sur PC. Il est beaucoup utilisé dans les
jeux de tirs ou de course.
Commencez par créer un nouveau projet basé sur XNA. Importez votre classe Sprite,
ajoutez une image au projet puis utilisez-la en créant un nouveau sprite.
=Labat FM.book Page 68 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


68

Figure 4-1
Récupérez facilement une
classe écrite précédemment

Pense-bête
Lorsque vous récupérerez une classe créée dans un précédent projet pour l’utiliser dans un nouveau,
n’oubliez pas de modifier la déclaration de l’espace de noms pour pouvoir l’utiliser sans devoir ajouter de
directive using au nouveau projet.

Comme les classes qui vont être utilisées ici font partie de l’espace de noms Microsoft
.Xna.Framework.Input, n’oubliez pas d’ajouter une directive using, cela simplifiera l’écriture
du code.
Le framework met à votre disposition l’objet Keyboard qui possède la méthode GetState()
et – comble de bonheur – il dispose également de la classe KeyboardState. Une fois l’état du
clavier récupéré, vous aurez accès à l’ensemble des touches pressées au moment de l’appel
à la méthode et pourrez également déterminer si une touche donnée est pressée ou non.
L’ensemble des touches de votre clavier (et même certaines touches auxquelles vous
n’avez jamais fait attention) sont disponibles via l’énumération Keys. Rappelons qu’une
énumération est un type de données constitué d’un ensemble de constantes. Voici un
exemple de déclaration d’énumération :
enum CouleurDesYeux
{
bleu,
marron,
vert,
gris
};
L’accès à une valeur de l’énumération se fait en écrivant le nom de l’énumération suivi
d’un point et de la valeur voulue :
CouleurDesYeux.bleu;
=Labat FM.book Page 69 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
69

Dans l’exemple suivant, l’utilisateur a maintenant la possibilité de quitter le jeu en appuyant


sur la touche Échap de son clavier (à laquelle nous accédons via Keys.Escape) :
protected override void Update(GameTime gameTime)
{
KeyboardState KState = Keyboard.GetState();

if (KState.IsKeyDown(Keys.Escape))
this.Exit();

base.Update(gameTime);
}
Vous pouvez également écrire une forme plus contractée, qui n’utilise pas de variable
pour stocker l’état du clavier. Il faut alors faire intervenir l’objet Keyboard comme suit :
if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();
Voyons à présent comment déplacer notre sprite. Ajoutons une variable qui contiendra sa
vitesse de déplacement.
float speed = 0.1f;
Le reste est très simple. Selon la touche fléchée sur laquelle il appuie, l’utilisateur
déplace le sprite dans la direction qu’il souhaite, tout en modulant cette translation par le
temps qui s’est écoulé depuis la dernière frame, comme vous l’avez vu au chapitre 3.
protected override void Update(GameTime gameTime)
{
KeyboardState KState = Keyboard.GetState();

if (KState.IsKeyDown(Keys.Left))
sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed, 0));
if (KState.IsKeyDown(Keys.Right))
sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds * speed,
➥ 0));
if (KState.IsKeyDown(Keys.Up))
sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));
if (KState.IsKeyDown(Keys.Down))
sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));

base.Update(gameTime);
}
Pour traiter une combinaison de touches, utilisez la méthode GetPressedKeys() qui renvoie
un tableau de Keys. L’exemple suivant affiche la liste des touches sur lesquelles le joueur
a appuyé à la place du titre de la fenêtre.
=Labat FM.book Page 70 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


70

Figure 4-2
Le joueur peut maintenant déplacer son sprite où bon lui semble.

KeyboardState KState = Keyboard.GetState();

this.Window.Title = "";
foreach (Keys key in KState.GetPressedKeys())
this.Window.Title += key.ToString();

La souris
Il est temps à présent d’étudier la souris. Ce périphérique est particulièrement adapté aux
jeux de tir et de gestion.
Cette fois-ci encore, vous pouvez utiliser un objet MouseState de manière à récupérer
l’état de la souris mis à disposition par l’objet Mouse. De la même manière que pour le
clavier, vous pouvez grâce à cet objet connaître l’état de la souris : les boutons utilisés, la
position de la souris ou encore le nombre d’interventions sur la molette.
Dans un premier temps, il faut modifier votre classe Sprite. Jusqu’à présent, vous ne pouviez
qu’appliquer des translations à votre Sprite or, dans l’exemple suivant, vous souhaitez
pouvoir modifier directement sa position. Ajoutez donc une propriété en lecture et en
écriture concernant la position de votre sprite.
Vector2 position;
public Vector2 Position
{
get { return position; }
set { position = value; }
}
=Labat FM.book Page 71 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
71

À présent, vous avez tous les outils en main pour transformer votre sprite en curseur. À
chaque appel de la méthode Update(), il suffit de remplacer la position du sprite par celle
de la souris.
protected override void Update(GameTime gameTime)
{
MouseState MState = Mouse.GetState();
sprite.Position = new Vector2(MState.X, MState.Y);
base.Update(gameTime);
}
Notez que, comme pour le clavier, vous n’êtes pas obligé de stocker l’état de la souris et
pouvez l’exploiter directement.
sprite.Position = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
En ce qui concerne les clics de la souris, vous les détectez en utilisant l’énumération
ButtonState qui prend deux états : Pressed ou Released.
if (MState.LeftButton == ButtonState.Pressed)
this.Window.Title = "Gauche";
if (MState.MiddleButton == ButtonState.Pressed)
this.Window.Title = "Milieu";
if (MState.RightButton == ButtonState.Pressed)
this.Window.Title = "Droit";
Pour finir, vous pouvez récupérer les mouvements qu’effectue l’utilisateur avec sa molette
via la propriété ScrollWheelValue. Celle-ci retourne une valeur entière qui s’incrémentera
si l’utilisateur fait tourner la molette vers le haut et qui se décrémentera dans le cas
contraire.
this.Window.Title = MState.ScrollWheelValue.ToString();

La manette de la Xbox 360


La manette de la Xbox 360 est le contrôleur de base que tous les joueurs de la console
possèdent. Si vous développez un jeu pour la console, vous devrez donc adapter son
fonctionnement à cette manette.
Le fonctionnement de la manette est similaire à celui du clavier et de la souris. Vous stockez
son état dans un objet de type GamePadState que vous récupérerez de l’objet GamePad.
Cependant, comme il peut y avoir plusieurs manettes connectées à la console, il faut
spécifier celle qui vous intéresse grâce à l’énumération PlayerIndex.
GamePadState GPState = GamePad.GetState(PlayerIndex.One);
Tout d’abord, vous pouvez vérifier qu’une manette est bien connectée à la console en
utilisant la propriété IsConnected.
GamePadState GPState = GamePad.GetState(PlayerIndex.Two);
if (GPState.IsConnected)
this.Window.Title = "La manette 2 n’est pas connectée à la console";
=Labat FM.book Page 72 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


72

Vous avez accès à deux méthodes classiques, l’une permettant de savoir si un bouton est
pressé, l’autre s’il ne l’est pas. Le choix du bouton se fait grâce à l’énumération Buttons,
laquelle vous permet également d’accéder à l’intégralité des boutons d’une manette
Xbox 360.
if (GPState.IsButtonDown(Buttons.A))
this.Exit();

if (GPState.IsButtonUp(Buttons.B))
this.Window.Title = "Le bouton B n’est pas pressé";
Comme d’habitude, il est tout à fait possible de s’affranchir du stockage de l’état de la
manette.
if (GamePad.GetState(PlayerIndex.One).IsButtonDown(Buttons.A))
this.Exit();
Vous pourriez également estimer l’état d’un bouton avec l’énumération ButtonState. Un
bouton a soit l’état Pressed, soit l’état Released.
if (GPState.Buttons.A == ButtonState.Pressed)
this.Exit();
De la même manière, vous pourrez récupérer l’état du pad directionnel. Le cas des
diagonales est géré, il peut donc y avoir deux directions pressées.
if (GPState.DPad.Down == ButtonState.Pressed)
Window.Title = "Vers le haut";
L’état des gâchettes analogiques gauche et droite peut aussi être récupéré. La propriété
renverra un float, compris entre 0 et 1, 1 signifiant que la gâchette est complètement
enfoncée. Cette variation de valeur pour être utilisée, par exemple, pour l’accélération ou
la décélération dans un jeu de course.
this.Window.Title = GPState.Triggers.Left.ToString();
En ce qui concerne les sticks analogiques, il est possible de récupérer un objet de type
Vector2 correspondant à la distance qui les sépare de la position initiale des sticks.
this.Window.Title = GPState.ThumbSticks.Left.X + " ; " + GPState.ThumbSticks.Left.Y;
Enfin, la fonction SetVibration de l’objet GamePad permet de faire vibrer la manette. Elle
attend comme paramètres la manette concernée, la vitesse à appliquer au moteur gauche
et celle à appliquer au moteur droit. Notez également que la fonction renvoie un booléen
vous indiquant si les vibrations ont eu lieu. Lorsque vous souhaitez arrêter les vibrations,
il vous suffira de passer une vitesse nulle en paramètre de la fonction.
L’exemple suivant fait vibrer la manette durant 5 secondes.
int time = 0;

protected override void Update(GameTime gameTime)


{
=Labat FM.book Page 73 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
73

time += gameTime.ElapsedGameTime.Milliseconds;
if (time < 5000)
{
if (!GamePad.SetVibration(PlayerIndex.One, 1, 1))
this.Window.Title = "Impossible de faire vibrer la manette";
}
else
this.Window.Title = "Temps écoulé";
base.Update(gameTime);
}
Vous ne le savez peut être pas mais la manette de la Xbox 360 peut également fonctionner
sur votre ordinateur ; il est possible d’en brancher jusqu’à quatre et bien entendu de les
utiliser dans XNA. Il faut pour cela utiliser un module USB que vous connecterez à votre
ordinateur.

Utilisation de périphériques spécialisés


La création d’un bon jeu passe par la définition d’un gameplay innovant. L’utilisation de
périphériques sortant de l’ordinaire permet d’accéder à cette phase d’innovation, nous en
donnons pour preuve les jeux de rythmes musicaux qui rencontrent un grand succès
depuis quelques années.
En ce qui concerne la Xbox 360, les périphériques spéciaux – qu’il s’agisse d’une guitare,
d’une batterie, d’un tapis de danse ou autre – agissent comme une manette sur la console.
C’est-à-dire qu’une guitare d’un jeu musical peut très bien faire office d’arme dans un jeu
de tir par exemple. Ce n’est donc pas plus dur de développer un jeu prévu pour utiliser un
de ces périphériques !
Vous devez toutefois faire attention à certains périphériques qui n’implémentent pas tous les
boutons d’une manette classique. Voici la liste (provenant de Microsoft) des composants
obligatoirement présents sur un périphérique :
• les boutons A, B, X et Y ;
• les boutons Back, Start et Xbox Guide ;
• le pad directionnel.
Cela signifie donc que toutes les autres parties d’une manette de Xbox 360, notamment
les gâchettes ou les sticks directionnels, ne sont pas toujours présentes sur les périphériques
de la console.

Les services avec XNA


La deuxième partie de ce chapitre va vous présenter ce qu’est un service dans XNA et
comment l’utiliser. Vous créerez enfin un composant qui vous permettra de récupérer
facilement tous vos services n’importe où dans le code de votre jeu.
=Labat FM.book Page 74 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


74

Les interfaces en C#
Une interface contient des prototypes de méthodes et de propriétés et peut ainsi être
comparée à un contrat. Une classe qui signe le contrat, c’est-à-dire qui implémente une
interface, s’engage à fournir une implémentation du contenu de l’interface.
Retournez dans un projet en mode console et imaginez par exemple l’interface présentée
ci-dessous. Remarquez bien que la méthode Identification() n’est pas définie, il y a
seulement son prototype.
public interface IUseless
{
string Identification();
}

Nom d’une interface


Ce n’est pas une règle officielle, mais généralement les développeurs font précéder le nom d’une interface
par un I.

Il n’est pas non plus précisé quel type de valeur est concerné : c’est un nouveau pas vers
la généricité. Dans l’exemple ci-dessous, deux classes qui implémentent cette interface
sont présentées.
class Machine : IUseless
{
string serialNumber;
public Machine(string serialNumber)
{
this.serialNumber = serialNumber;
}
public string Identification()
{
return serialNumber;
}
}
class Human : IUseless
{
string name;
string firstName;
public Human(string name, string firstName)
{
this.name = name;
this.firstName = firstName;
}
public string Identification()
{
return firstName + " " + name;
}
}
=Labat FM.book Page 75 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
75

La signature du contrat s’écrit donc comme un héritage (voir le chapitre 3 à ce sujet) :


Classe : Interface
Cependant, à la différence de l’héritage, une classe peut implémenter plusieurs interfaces.
La syntaxe serait la suivante :
Classe : InterfaceA, InterfaceB
L’utilisation des classes se fait d’une manière tout à fait classique.
class Program
{
static void Main(string[] args)
{
Human human = new Human("Dylan", "Bob");
Console.WriteLine(human.Identification());
Machine machine = new Machine("B563RF2");
Console.WriteLine(machine.Identification());
Console.Read();
}
}

Comment utiliser les services


Les services ont été conçus pour récupérer, à partir de votre classe principale (celle qui
hérite de Game), un objet qui implémente une interface donnée. Leur avantage est donc de
faciliter l’utilisation des méthodes d’un objet sans avoir à le passer sans cesse en paramètre
et sans utiliser de variable statique.
La gestion des services se fait tout simplement par la propriété Services de la classe Game.
Elle correspond à un objet de type GameServiceContainer qui dispose des trois méthodes
AddService(), GetService() et RemoveService(). Les services sont en fait stockés dans un
objet Dictionary<Type, Object>.
Vous allez maintenant créer votre premier service qui vous servira à gérer le clavier. Dans
la première partie de ce chapitre, nous avons présenté les deux possibilités qui existent
pour récupérer les entrées de l’utilisateur : à savoir utiliser directement l’objet Keyboard
ou alors passer par un objet KeyboardState. Or, dès que vous commencerez à travailler sur
un projet conséquent, vous vous rendrez compte que l’accès répété à l’objet Keyboard peut
être à l’origine de pertes de performances. En fait, vous pourriez n’y accéder qu’une fois
et mettre à disposition de tout le monde l’objet KeyboardState : cela correspond parfaitement
à la définition des services.
L’écriture du contrat se fait très rapidement : une fonction suffit pour savoir si une touche
est effectivement pressée.
interface IKeyboardService
{
bool IsKeyDown(Keys key);
}
=Labat FM.book Page 76 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


76

Maintenant, ajoutez une nouvelle classe au projet et nommez-la KeyboardService. Cette


classe devra implémenter l’interface IKeyboardService, mais devra aussi hériter de la classe
GameComponent, laquelle implémente les interfaces IGameComponent et IUpdateable. Ces inter-
faces imposent respectivement l’implémentation des méthodes Initialize() et Update().
La classe Game dispose justement d’une collection de GameComponent, ainsi les méthodes
citées précédemment seront automatiquement appelées pour les objets de cette collection.
Vous retrouverez ici la classe KeyboardService. Il n’y a rien de particulier à signaler ici :
dans le constructeur, on se contente d’ajouter la classe courante à la collection de service
de l’objet Game et dans la méthode Update(), on enregistre l’état du clavier. Enfin, la fonction
IsKeyDown() se contentera de consulter la méthode éponyme de l’objet KBState.
class KeyboardService : GameComponent, IKeyboardService
{
KeyboardState KBState;

public KeyboardService(Game game)


: base(game)
{
game.Services.AddService(typeof(IKeyboardService), this);
}

bool IsKeyDown(Keys key)


{
return KBState.IsKeyDown(key);
}

public override void Update(GameTime gameTime)


{
KBState = Keyboard.GetState();
base.Update(gameTime);
}
}
Ajoutez, dans la méthode Initialize() de votre classe Game, un nouvel objet KeyboardService
à la collection de Components.
this.Components.Add(new KeyboardService(this));
Dans la méthode Update(), vous allez devoir accéder à votre service. La ligne suivante
sert à récupérer un service depuis la collection de la classe Game. Il est important de garder
en mémoire que la collection est un dictionnaire et que les valeurs sont donc indexées par
type : il faut préciser celui de notre service, c’est le but de l’opérateur typeof.
this.Services.GetService(typeof(IKeyboardService))
Cependant, à ce stade, l’objet que vous récupérez n’est toujours pas du type IKeyboardService,
vous allez donc devoir le convertir. Cette opération s’appelle le casting : en précisant le
=Labat FM.book Page 77 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
77

type voulu entre parenthèses juste avant l’objet, vous pourrez accéder aux méthodes de
l’objet.

Le casting
Lorsqu’il est nécessaire de convertir une donnée dans un type différent de celui choisi par la conversion
automatique, il faut préciser de manière explicite le nouveau type : cette opération s’appelle le casting.
Attention, cela peut parfois être source d’une perte de données. Si vous convertissez, par exemple, un
nombre décimal de type float vers un int, vous perdrez la partie décimale du nombre.

((IKeyboardService)this.Services.GetService(typeof(IKeyboardService)))
➥ .IsKeyDown(Keys.Up)
Il est à présent possible de réécrire la méthode du début de ce chapitre.
protected override void Update(GameTime gameTime)
{
if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService)))
➥ .IsKeyDown(Keys.Up))
sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));

if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService)))
➥ .IsKeyDown(Keys.Down))
sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));

if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService)))
➥ .IsKeyDown(Keys.Left))
sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed, 0));

if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService)))
➥ .IsKeyDown(Keys.Right))
sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed, 0));

base.Update(gameTime);
}
Enfin, notez qu’il n’est pas obligatoire d’écrire une interface pour un service. Il est possible
de procéder directement ainsi :
game.Services.AddService(typeof(KeyboardService), new KeyboardService());  

Les méthodes génériques


Comme vous venez de le constater, récupérer un service pour l’utiliser peut être
ennuyeux à la longue. Vous allez maintenant apprendre à créer une classe qui vous en
simplifiera l’utilisation.
=Labat FM.book Page 78 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


78

Les méthodes génériques permettent d’écrire des méthodes qui effectueront exactement
le même traitement, peu importe le type des paramètres passés. Vous avez utilisé une
méthode générique chaque fois que vous avez chargé une texture pour vos sprites.
texture = content.Load<Texture2D>(assetName);
La déclaration d’une telle classe se fait de la manière suivante :
public static void Sample<T>(T param)
{
//…
}
Vous pouvez également ajouter des conditions sur le type des paramètres. Le tableau 4-1
en dresse une liste :

Tableau 4-1 Différentes conditions possibles sur le type des paramètres

Contrainte Description
where T : struct Le type T doit être un type valeur.

where T : class Le type T doit être un type référence.

where T : new() Le type T doit disposer d’un constructeur public sans paramètres.

where T : <classe de base> Le type T doit être dérivé de la classe spécifiée.

where T : <interface> Le type T doit implémenter l’interface spécifiée.

Ajoutez une nouvelle classe au projet et nommez-la ServiceHelper. Elle fera office de
classe statique et contiendra un champ statique qui sera utilisé pour stocker une référence
vers votre classe Game.
static class ServiceHelper
{
static Game game;

public static Game Game


{
set { game = value; }
}
}
Ajoutez une méthode statique et générique : elle devra ensuite ajouter à la collection de
services l’objet qu’elle a reçu en paramètre.
public static void Add<T>(T service) where T : class
{
game.Services.AddService(typeof(T), service);
}
=Labat FM.book Page 79 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
79

Il faut ensuite ajouter une seconde méthode pour récupérer un service ; elle sera également
statique et générique. Notez l’utilisation du mot-clé as, utile à la conversion de types
références.
public static T Get<T>() where T : class
{
return game.Services.GetService(typeof(T)) as T;
}
Ci-dessous, le code complet de la classe.
static class ServiceHelper
{
static Game game;

public static Game Game


{
set { game = value; }
}

public static void Add<T>(T service) where T : class


{
game.Services.AddService(typeof(T), service);
}

public static T Get<T>() where T : class


{
return game.Services.GetService(typeof(T)) as T;
}
}
Vous devez maintenant modifier le constructeur de la classe KeyboardService pour qu’il
passe par la classe ServiceHelper, plutôt qu’ajouter directement le service à la collection
de l’objet Game.
public KeyboardService(Game game)
: base(game)
{
ServiceHelper.Add<IKeyboardService>(this);
}
Pour rendre cette classe utilitaire opérationnelle, il vous reste à définir la référence vers
votre classe Game.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
}
=Labat FM.book Page 80 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


80

À présent, tout est prêt pour que vous puissiez utiliser plus simplement vos services.
Ci-dessous, vous trouverez le corps de la méthode Update() dans sa nouvelle version.
Beaucoup plus lisible, n’est-ce pas ?
protected override void Update(GameTime gameTime)
{
if(ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Up))
sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Down))
sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Left))
sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed, 0));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Right))
sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds *
➥ speed, 0));

base.Update(gameTime);
}

Toujours plus d’interactions grâce à la GUI


Pour conclure, sachez que les interactions avec le joueur ne passent pas seulement par les
périphériques, mais également par le gameplay et les mécanismes de jeux que vous
pouvez implémenter.
L’un des points les plus importants dans un jeu réside dans la communication avec le
joueur, qui peut s’exercer par l’intermédiaire d’une GUI (Graphical User Interface).

Graphical User Interface


Les GUI ont été présentes dès les débuts du jeu vidéo, en effet ceux-ci on toujours eu besoin de donner
des indications aux joueurs. Mais ces dernières ont fortement évolué depuis Pong, leur aspect graphique
en faisant parfois de véritables petites œuvres d’art.
Aujourd’hui la tendance est plutôt de limiter fortement le nombre d’informations affichées à l’écran, afin
d’augmenter le sentiment d’immersion (par exemple dans les jeux Gears of War ou Mirror’s Edge).
Cependant, ce comportement n’est adapté qu’à certains types de jeux (jeux à la première personne
notamment). Sauf gameplay particulier, il est difficile d’imaginer un jeu de gestion qui ne présente aucune
information au joueur.
=Labat FM.book Page 81 Vendredi, 19. juin 2009 4:01 16

Interactions avec le joueur


CHAPITRE 4
81

Il existe de nombreux projets de GUI disponibles sur Internet et utilisables dans vos jeux.
En voici deux :
• XNA Simple Gui (http://www.codeplex.com/simplegui) qui, comme son nom l’indique, arbore
un design très simpliste mais possède néanmoins une liste de contrôles assez intéres-
sante (panel, boutons, etc.).
• WinForms (http://www.ziggyware.com/news.php?readmore=374) vous propose une grande liste
de contrôles (et même des barres de progression ou des potentiomètres), ainsi qu’un
design proche de celui des fenêtres de Windows XP.

Figure 4-3
Le projet xWinForms, une interface graphique pour XNA

Il est également possible de faire interagir les joueurs entre eux en local ou même en
réseau. Ces deux notions seront abordées au chapitre 11.

En résumé
Dans ce chapitre, vous avez découvert les différents périphériques compatibles avec XNA
et leur utilisation dans vos jeux. Vous avez ensuite abordé la notion de services, que vous
avez mise en pratique pour pouvoir exploiter plus facilement le clavier dans vos jeux.
=Labat FM.book Page 82 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 83 Vendredi, 19. juin 2009 4:01 16

5
Cas pratique :
programmer un Pong

Vous disposez à présent des compétences nécessaires pour vous lancer dans la création
d’un premier vrai petit jeu. Dans ce chapitre nous allons donc récrire le jeu mythique
qu’est devenu Pong. Passage obligé du développeur amateur, ce projet a pour but de vous
faire découvrir les pratiques en amont du développement d’un jeu vidéo, puis mettra
directement en application toutes les notions découvertes précédemment.

Avant de se lancer dans l’écriture du code


Avant toute chose, et ceci est valable même pour le jeu le plus basique qui soit, vous
devez tout planifier. La plupart des projets, dans le monde du jeu vidéo ou ailleurs,
échouent parce que cette phase de préparation a été négligée : certains arrivent à terme,
mais fournissent un résultat différent de celui attendu, d’autres n’ont même pas cette chance.
Même si l’exemple de ce chapitre a l’air simpliste, il vous permettra d’apprendre à prendre
les choses en mains dès le début et de gagner du temps pour l’étape de développement.

Définir le principe du jeu


Pong est un jeu inspiré du tennis de table qui a fait son apparition dans les années 1970,
d’abord sur une borne d’arcade puis en console de salon.
Il existe certainement des milliers de versions de Pong. Comme nous l’avons précisé dans
l’introduction de ce chapitre, il s’agit du premier jeu que la plupart des développeurs
réalisent. L’engouement que les programmeurs ont pour ce jeu contribue sans cesse à son
=Labat FM.book Page 84 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


84

renouvellement : on peut même trouver des versions massivement multi-joueur de Pong


sur l’Internet.
Le principe que nous allons implémenter est simple : il y a deux joueurs, chacun contrôlant
une raquette. Les deux joueurs s’envoient une balle qui rebondit sur les bords haut et bas
de l’écran. Si la balle touche la raquette d’un des deux joueurs, elle repart vers l’autre
joueur, cependant, si elle manque la raquette, l’autre joueur gagne.
La version originale de Pong était plus complète puisqu’elle possédait son et affichage du
score : deux facettes d’XNA que nous n’avons pas encore abordées.

Formaliser en pseudo-code
Maintenant que le principe de jeu est clair, il est possible de traduire le déroulement d’une
partie en pseudo-code. Ce cycle se répétant tant que le joueur n’aura pas quitté le jeu.
Tant que le joueur n’a pas lancé la partie
Attendre
Fin tant que

Tant que la balle ne sort pas par la gauche ou la droite de l’écran


Si la balle touche le haut ou le bas de l’écran
Faire rebondir la balle
Fin si
Si la balle touche une raquette
Faire rebondir la balle
Fin si
Fin Tant que
La figure 5-1 représente les classes bat (raquette) et ball (balle) ainsi que leurs différents
champs. Ces deux classes héritent de la classe Sprite.

Figure 5-1
Diagramme des classes bat et ball
=Labat FM.book Page 85 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
85

L’arrière-plan du jeu pourrait être géré en créant un simple sprite, puisqu’il n’a pas de
propriétés particulières. Cependant, pour factoriser le code de notre classe Game, nous
implémenterons tout de même une classe qui lui sera dédiée. Cette classe dérivera de la
classe DrawableGameComponent, qui hérite elle-même de la classe GameComponent et qui en
plus implémente l’interface IDrawable, nous fournissant la méthode Draw().

Développement du jeu
Maintenant que vous avez précisé vos objectifs, il est temps de passer à la pratique et de
les réaliser.

Création du projet
Dans ce chapitre, il sera question d’un projet pour Windows, mais vous pourrez facilement
l’adapter pour Xbox 360 en vous aidant du chapitre précédent si c’est nécessaire.
1. Commencez par créer un nouveau projet que vous baptiserez « Pong ».
2. Renommez le fichier Game1.cs en Pong.cs et, lorsque Visual Studio vous demande si
vous souhaitez également renommer toutes les références à « Game1 », acceptez.
Votre classe Game1 s’appelle maintenant Pong.

Figure 5-2
Visual Studio peut renommer automatiquement toutes les références à un élément

3. Ensuite, ajoutez les éléments développés dans les chapitres précédents, c’est-à-dire
les fichiers IKeyboardService.cs, KeyboardService.cs, ServiceHelper.cs et Sprite.cs.
4. Dans votre classe Pong, pensez à définir la propriété Game de la classe ServiceHelper,
et à ajouter le composant KeyboardService à la collection.
public Pong()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
}
Votre projet doit donc à présent ressembler à celui visible sur la figure 5-3.
=Labat FM.book Page 86 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


86

Figure 5-3
Vous récupérerez souvent des éléments développés précédemment pour vos nouveaux projets

L’arrière-plan
Ajoutez une nouvelle classe au projet que vous nommerez Background et qui dérivera de
DrawableGameComponent.
class Background : DrawableGameComponent
{
public Background(Game game)
: base(game)
{
}

public override void Initialize()


{
base.Initialize();
}

protected override void LoadContent()


{
base.LoadContent();
}

public override void Draw(GameTime gameTime)


{
base.Draw(gameTime);
}
}
Pour pouvoir dessiner l’arrière-plan, vous aurez besoin d’un SpriteBatch puisque vous ne
disposez pas de celui présent dans la classe Pong. Ajoutez donc un champ de ce type que
vous initialiserez dans la méthode Initialize(). Vous pouvez récupérer l’objet GraphicsDevice
de la classe Pong en utilisant la propriété Game que met à disposition la classe parente.
=Labat FM.book Page 87 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
87

SpriteBatch spriteBatch;

public override void Initialize()


{
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
base.Initialize();
}
Il vous faudra finalement un sprite qui correspondra à l’image de l’arrière-plan. Déclarez
un champ de type Sprite, chargez l’image voulue pour l’arrière-plan et dessinez-le.
Notez que pour le chargement de l’image, vous pouvez récupérer le ContentManager de la
classe Pong via la propriété Game.
Voici donc le code final de la classe Background.
class Background : DrawableGameComponent
{
Sprite sprite;
SpriteBatch spriteBatch;

public Background(Game game)


: base(game)
{
}

public override void Initialize()


{
sprite = new Sprite(new Vector2(0, 0));
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
base.Initialize();
}

protected override void LoadContent()


{
sprite.LoadContent(Game.Content, "back");
base.LoadContent();
}

public override void Draw(GameTime gameTime)


{
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
Puis, dans le constructeur de la classe Pong, ajoutez un nouvel objet de type Background à
la collection Components.
=Labat FM.book Page 88 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


88

public Pong()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
Components.Add(new Background(this));
}

Les raquettes
Il est temps d’écrire le code de la classe Bat. Celle-ci dérivera bien sûr de la classe Sprite.
Implémentez dès maintenant les champs présentés dans le diagramme de classes de la
première partie de ce chapitre.
class Bat : Sprite
{
float speed;
int maxHeight;
Keys keyUp;
Keys keyDown;

public Bat(Vector2 position, int playerNumber, int maxHeight, Keys keyUp, Keys
➥ keyDown)
: base(position)
{
this.playerNumber = playerNumber;
this.maxHeight = maxHeight;
this.keyUp = keyUp;
this.keyDown = keyDown;
speed = 0.7f;
}
}
Maintenant, vous devez ajouter une méthode Update() à cette classe. Passez-lui un objet
GameTime en paramètre de manière à moduler la vitesse de déplacement des raquettes.
Utilisez donc la classe ServiceHelper pour savoir si le joueur a appuyé sur une touche, si
c’est le cas, vérifiez que la raquette n’est pas sur l’extrémité haute ou basse de l’écran
avant de déplacer le sprite.
Pour vérifier si la raquette est en haut ou en bas de l’écran, rappelez-vous que l’origine du
repère à l’écran est située en haut à gauche (figure 5-4) et que la position d’un sprite
correspond au coin supérieur gauche de l’image. Dans les calculs, il faut donc considérer
que l’extrémité haute de l’écran se situe aux points de coordonnées (x ; 0) et l’extrémité
basse aux points de coordonnées (y ; maxHeight – textureHeight).
Voici donc le code source de la classe Bat.
=Labat FM.book Page 89 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
89

class Bat : Sprite


{
float speed;
int maxHeight;
Keys keyUp;
Keys keyDown;

public Bat(Vector2 position, int playerNumber, int maxHeight, Keys keyUp, Keys
➥ keyDown)
: base(position)
{
this.maxHeight = maxHeight;
this.keyUp = keyUp;
this.keyDown = keyDown;
speed = 0.7f;
}

public void Update(GameTime gameTime)


{
if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(keyDown))
if (Position.Y < (maxHeight - Texture.Height))
Position = new Vector2(Position.X, Position.Y + speed *
➥ gameTime.ElapsedGameTime.Milliseconds);

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(keyUp))
if (Position.Y > 0)
Position = new Vector2(Position.X, Position.Y - speed *
➥ gameTime.ElapsedGameTime.Milliseconds);
}
}
Retrouvez ci-dessous le code source de la classe Pong qui utilise deux raquettes. La position
des sprites n’est déterminée qu’après avoir chargé leur texture, ce qui est normal puisque
cette position dépend de la taille de la texture. Notez enfin que le dessin des raquettes se
fait après l’appel à la méthode Draw() de la classe parente, uniquement pour respecter
l’ordre des éléments à l’écran : l’arrière-plan doit être dessiné avec les autres éléments.
public class Pong : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Bat playerOne;
Bat playerTwo;

public Pong()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
=Labat FM.book Page 90 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


90

Components.Add(new Background(this));

playerOne = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight,


➥ Keys.A, Keys.Q);
playerTwo = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight,
➥ Keys.Up, Keys.Down);
}

protected override void Initialize()


{
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
playerOne.LoadContent(Content, "bat");
playerOne.Position = new Vector2(20, graphics.PreferredBackBufferHeight /
➥ 2 - playerOne.Texture.Height / 2);
playerTwo.LoadContent(Content, "bat");
playerTwo.Position = new Vector2(770, graphics.PreferredBackBufferHeight /
➥ 2 - playerTwo.Texture.Height / 2);
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
playerOne.Update(gameTime);
playerTwo.Update(gameTime);
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

base.Draw(gameTime);

spriteBatch.Begin();
playerOne.Draw(spriteBatch);
playerTwo.Draw(spriteBatch);
spriteBatch.End();
}
}
Vous devez également modifier la classe Sprite et créer une propriété en lecture pour
l’objet texture, sans quoi vous ne pourrez pas récupérer ses dimensions.
=Labat FM.book Page 91 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
91

public Texture2D Texture


{
get { return texture; }
}

La balle
La classe Ball dérive également de la classe Sprite. Cette fois encore, implémentez les
champs présentés dans le diagramme du début de ce chapitre.
class Ball : Sprite
{
float speed;
Vector2 angle;
int maxHeight;
int maxWidth;

public Ball(Vector2 position, int maxWidth, int maxHeight)


: base(position)
{
this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
speed = 0.2f;
angle = new Vector2(1, 1);
}
}
Écrivons maintenant la méthode Update(). Celle-ci prendra en paramètre les raquettes des
deux joueurs, ainsi qu’une référence vers un booléen qui détermine si le jeu est en pause
ou non.
En premier lieu, calculez la nouvelle position de la balle en fonction de la vitesse, du
temps et de la direction.
Position = new Vector2(Position.X + speed * angle.X *
➥ gameTime.ElapsedGameTime.Milliseconds, Position.Y + speed * angle.Y *
➥ gameTime.ElapsedGameTime.Milliseconds);
Récupérer les raquettes des joueurs vous sera utile pour construire des objets Rectangle.
Ces objets disposent d’une méthode Intersects() vous permettant de détecter les collisions
entre la balle et les raquettes. Un objet Rectangle se construit tout simplement à partir de
deux coordonnées x et y, une largeur et une hauteur.
Rectangle ballRect = new Rectangle((int)Position.X, (int)Position.Y, Texture.Width,
➥ Texture.Height);
Rectangle playerOneRect = new Rectangle((int)playerOne.Position.X,
➥ (int)playerOne.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
Rectangle playerTwoRect = new Rectangle((int)playerTwo.Position.X,
➥ (int)playerTwo.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
=Labat FM.book Page 92 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


92

Il reste maintenant à tester les différents cas de figure : si la balle touche le haut de la
fenêtre ou le bas, si elle entre en collision avec une raquette ou bien si elle a atteint les
extrémités gauche ou droite de l’écran. Dans le cas d’une collision, on modifiera la direction.
class Ball : Sprite
{
float speed;
Vector2 angle;
int maxHeight;
int maxWidth;
public Ball(Vector2 position, int maxWidth, int maxHeight)
: base(position)
{
this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
speed = 0.2f;
angle = new Vector2(1, 1);
}
public void Update(GameTime gameTime, Sprite playerOne, Sprite playerTwo, ref
➥ bool started)
{
Position = new Vector2(Position.X + speed * angle.X *
➥ gameTime.ElapsedGameTime.Milliseconds, Position.Y + speed * angle.Y *
➥ gameTime.ElapsedGameTime.Milliseconds);
Rectangle ballRect = new Rectangle((int)Position.X, (int)Position.Y,
➥ Texture.Width, Texture.Height);
Rectangle playerOneRect = new Rectangle((int)playerOne.Position.X, (int)
➥ playerOne.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
Rectangle playerTwoRect = new Rectangle((int)playerTwo.Position.X, (int)
➥ playerTwo.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
// est-ce qu’il y a collision avec le haut de l’écran ?
if (Position.Y <= 0)
angle = new Vector2(angle.X, 1);
// ... ou bien avec le bas de l’écran ?
else if (Position.Y >= maxHeight - Texture.Height)
angle = new Vector2(angle.X, -1);
// ... ou alors avec le joueur 1 ?
else if (ballRect.Intersects(playerOneRect))
angle = new Vector2(1, angle.Y);
// ... ou bien le joueur 2 ?
else if (ballRect.Intersects(playerTwoRect))
angle = new Vector2(-1, angle.Y);
// ... ou bien l’extrémité gauche ou l’extrémité droite de l’écran a été
➥ atteinte
else if (Position.X <= 0 || Position.X + Texture.Width >= maxWidth)
started = false;
}
}
=Labat FM.book Page 93 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
93

De retour dans la classe Pong, déclarez un objet de type Ball, chargez sa texture et dessinez-
la. Ajoutez aussi un booléen et initialisez-le à false. La méthode Update() va légèrement
se compliquer. Le programme devra attendre que le joueur appuie sur la barre d’espace
avant de lancer la balle et rendre le mouvement des raquettes possible. Dans l’appel à la
méthode Update() de la balle, vous devez passer une référence vers votre booléen. Ceci se
fait grâce à l’instruction ref.
public class Pong : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Bat playerOne;
Bat playerTwo;
Ball ball;
bool started;

public Pong()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
Components.Add(new Background(this));

playerOne = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight,


➥ Keys.A, Keys.Q);
playerTwo = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight,
➥ Keys.Up, Keys.Down);

ball = new Ball(new Vector2(0, 0), graphics.PreferredBackBufferWidth,


➥ graphics.PreferredBackBufferHeight);

started = false;
}

protected override void Initialize()


{
Window.Title = "Appuyez sur espace pour démarrer";

base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
playerOne.LoadContent(Content, "bat");
playerOne.Position = new Vector2(20, graphics.PreferredBackBufferHeight /
➥ 2 - playerOne.Texture.Height / 2);
playerTwo.LoadContent(Content, "bat");
=Labat FM.book Page 94 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


94

playerTwo.Position = new Vector2(770, graphics.PreferredBackBufferHeight /


➥ 2 - playerTwo.Texture.Height / 2);
ball.LoadContent(Content, "ball");
ball.Position = new Vector2((graphics.PreferredBackBufferWidth / 2) -
➥ (ball.Texture.Width / 2), (graphics.PreferredBackBufferHeight / 2) -
➥ (ball.Texture.Height / 2));
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
base.Update(gameTime);

if (started)
{
playerOne.Update(gameTime);
playerTwo.Update(gameTime);
ball.Update(gameTime, playerOne, playerTwo, ref started);
}
else
{
if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Space))
{
started = true;
Window.Title = "Pong !";
}
}
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

base.Draw(gameTime);

spriteBatch.Begin();
playerOne.Draw(spriteBatch);
playerTwo.Draw(spriteBatch);
ball.Draw(spriteBatch);
spriteBatch.End();
}
}
Votre jeu est maintenant prêt. Vous pouvez le compiler et l’essayer pendant des heures…
Ou peut-être un peu moins !
=Labat FM.book Page 95 Vendredi, 19. juin 2009 4:01 16

Cas pratique : programmer un Pong


CHAPITRE 5
95

Figure 5-4
Repère 2D utilisé pour
l’affichage

Figure 5-5
Votre premier jeu n’est pas
si mal, n’est ce pas ?

Améliorer l’intérêt du jeu


Votre premier jeu est maintenant terminé, mais pour l’instant il n’a absolument rien
d’original et est commun à tous les jeux Pong. Pourquoi ne pas essayer d’en augmenter
la difficulté ?
Pour commencer, augmentez la vitesse de la balle à chaque fois qu’elle entre en collision
avec une raquette.
public void Update(GameTime gameTime, Sprite playerOne, Sprite playerTwo, ref bool
➥ started)
{
Position = new Vector2(Position.X + speed * angle.X * gameTime
➥ .ElapsedGameTime.Milliseconds, Position.Y + speed * angle.Y *
➥ gameTime.ElapsedGameTime.Milliseconds);

Rectangle ballRect = new Rectangle((int)Position.X, (int)Position.Y,


➥ Texture.Width, Texture.Height);
Rectangle playerOneRect = new Rectangle((int)playerOne.Position.X,
➥ (int)playerOne.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
=Labat FM.book Page 96 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


96

Rectangle playerTwoRect = new Rectangle((int)playerTwo.Position.X,


➥ (int)playerTwo.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);

// est-ce qu’il y a collision avec le haut de l’écran ?


if (Position.Y <= 0)
angle = new Vector2(angle.X, 1);
// ... ou bien avec le bas de l’écran ?
else if (Position.Y >= maxHeight - Texture.Height)
angle = new Vector2(angle.X, -1);
// ... ou alors avec le joueur 1 ?
else if (ballRect.Intersects(playerOneRect))
{
angle = new Vector2(1, angle.Y);
speed += 0.02f;
}
// ... ou bien le joueur 2 ?
else if (ballRect.Intersects(playerTwoRect))
{
angle = new Vector2(-1, angle.Y);
speed += 0.02f;
}
// ... ou alors, l’extrémité gauche ou l’extrémité droite de l’écran a été
➥ atteinte
else if (Position.X <= 0 || Position.X + Texture.Width >= maxWidth)
started = false;
}
Les possibilités d’évolution sont vraiment nombreuses (création d’une intelligence
artificielle, ajout de nouvelles balles, etc.), contentez-vous de laisser libre cours à votre
imagination, mais gardez toujours à l’esprit que ce n’est pas la débauche de technique qui
fait qu’un jeu est bon ou non, le gameplay est vraiment important !

En résumé
Dans ce chapitre, vous avez découvert que la phase de préparation (ou d’élaboration du
cahier des charges) ne doit pas être oubliée. Vous avez ensuite mené à bien votre premier
projet avec XNA, découvrant au passage comment créer un DrawableComponent et
gérer les collisions de façon primitive.
=Labat FM.book Page 97 Vendredi, 19. juin 2009 4:01 16

6
Enrichir les sprites :
textures, défilement,
transformation, animation

Dans le chapitre 3, nous avons commencé notre étude des sprites en XNA. Cependant, il
ne s’agissait que d’un simple aperçu des possibilités qu’offre le framework.
Dans ce chapitre nous allons découvrir les spécificités concernant l’affichage des textures
à l’écran et en profiterons pour améliorer notre classe Sprite. Enfin, nous découvrirons
comment dessiner du texte à l’écran.

Préparation de votre environnement de travail


Avant de se lancer dans le perfectionnement de vos connaissances des images dans XNA,
vous devez d’abord préparer votre environnement de travail.
Créez tout d’abord un nouveau projet baptisé « ChapitreSix », renommez la classe Game1
en ChapitreSix et importez la classe Sprite du chapitre 3. Pensez également à modifier
l’espace de noms. Ajoutez ensuite une image au gestionnaire de contenu. Pour le début
de ce chapitre, c’est l’image GameThumbnail.png, située à la racine de votre projet, qui sera
utilisée. Créez alors un sprite qui utilise cette image et affichez-le. Votre projet devrait
maintenant ressembler à celui visible sur la figure 6-1. Récapitulons ci-dessous le code
de la classe ChapitreSix.
=Labat FM.book Page 98 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


98

Figure 6-1
Votre projet devrait ressembler
à ceci

public class ChapitreSix : Microsoft.Xna.Framework.Game


{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Sprite sprite;
public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
sprite = new Sprite(100, 100);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
sprite.LoadContent(Content, "GameThumbnail");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
=Labat FM.book Page 99 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
99

La méthode Draw() de la classe SpriteBatch dispose de sept surcharges. Cependant, jusqu’à


présent nous n’en avons utilisé qu’une seule…
spriteBatch.Draw(texture, position, Color.White);
Rappelons, pour mémoire, que texture correspond à la texture à afficher, position à la
position du coin supérieur gauche de l’image et Color.White à la teinte à appliquer à la
texture, celle utilisée ici correspondant à une teinte nulle.

Figure 6-2
Un sprite affiché simple-
ment avec la classe Sprite
actuelle

Texturer un objet Rectangle


La surcharge de la méthode Draw() que nous savons utiliser pour le moment est classée
comme étant la deuxième par Visual Studio. À quoi correspond donc la première ? Faites
simplement défiler l’ensemble des surcharges avec les flèches haut et bas de votre clavier.
Les paramètres attendus par cette première surcharge sont visibles sur la figure 6-3.

Figure 6-3
Le détail de la première surcharge

Seul le deuxième paramètre diffère de la surcharge que nous connaissons déjà. Là où la


méthode attendait un Vector2 correspondant à la position du coin supérieur gauche de la
texture, elle attend à présent un objet de type Rectangle nommé destinationRectangle.
Nous avons croisé des objets de type Rectangle au chapitre précédent. Ils nous ont servi à
déterminer s’il y avait collision entre les raquettes et la balle. Un rectangle comprenait en
=Labat FM.book Page 100 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


100

fait une paire de coordonnées X et Y, ainsi qu’une largeur et une hauteur. Nous pouvons
donc facilement déduire le rôle de ce rectangle dans cette surcharge : la texture le
remplira, c’est-à-dire qu’elle occupera sa position et qu’elle s’adaptera à sa taille, en se
redimensionnant si nécessaire.

Modifier la classe Sprite


Assez parlé, il est temps d’appliquer une texture à notre Rectangle.
1. Ajoutez un champ pour un objet de type Rectangle à votre classe Sprite ainsi qu’une
propriété en lecture et écriture.
Rectangle destinationRectangle;
public Rectangle DestinationRectangle
{
get { return destinationRectangle; }
set { destinationRectangle = value; }
}
2. Surchargez maintenant le constructeur de la classe pour qu’il prenne un objet
Rectangle en paramètre plutôt qu’une unique paire de coordonnées.
public Sprite(Rectangle destinationRectangle)
{
this.destinationRectangle = destinationRectangle;
}

3. Et enfin, modifiez la méthode Draw() pour qu’elle utilise la première surcharge.


public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, destinationRectangle, Color.White);
}
4. Retournez dans la classe ChapitreSix et utilisez maintenant votre nouveau constructeur,
puis exécutez le programme.
protected override void Initialize()
{
sprite = new Sprite(new Rectangle(100, 100, 300, 300));
base.Initialize();
}

Rien ne vous oblige à conserver les proportions de votre sprite ! L’exemple suivant
redimensionne le sprite en modifiant sa hauteur.
protected override void Initialize()
{
sprite = new Sprite(new Rectangle(100, 100, 300, 100));
base.Initialize();
}
=Labat FM.book Page 101 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
101

Figure 6-4
La taille de la texture est
facilement modifiable

Figure 6-5
La texture est maintenant
aplatie

Maintenant que nous avons fait le tour de la première surcharge, passons à la troisième.
Celle-ci prend un nouveau paramètre en compte, de type Rectangle? et s’appelle
sourceRectangle. Le point d’interrogation signifie que le paramètre peut être nul.

Figure 6-6
Le nouveau paramètre de la troisième surcharge
=Labat FM.book Page 102 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


102

Comme vous pouvez le voir en anglais dans la description visible sur la figure 6-6, ce
rectangle permet de sélectionner la portion de la texture qui devra être dessinée. Si vous
décidez de passer null plutôt qu’un objet Rectangle, c’est toute la texture qui sera dessinée.
Testons cette nouvelle fonctionnalité :
1. Commencez par ajouter un nouveau champ à la classe. Il devra être de type Rectangle?
et s’appeler sourceRectangle. Pensez également à ajouter des propriétés en lecture et
en écriture pour cette nouvelle variable.
Rectangle? sourceRectangle = null;
public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}
2. Comme d’habitude, ajoutez maintenant une nouvelle surcharge du constructeur de la
classe Sprite.
public Sprite(Rectangle destinationRectangle, Rectangle? sourceRectangle)
{
this.destinationRectangle = destinationRectangle;
this.sourceRectangle = sourceRectangle;
}
3. Et enfin, modifiez la méthode Draw().
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, Color.White);
}
4. La classe Sprite étant prête, il ne reste plus qu’à modifier la classe ChapitreSix qui
l’utilise. Pour commencer, contentez vous de passer null comme nouveau paramètre.
protected override void Initialize()
{
sprite = new Sprite(new Rectangle(100, 100, 64, 64), null);
base.Initialize();
}
À présent, essayez d’afficher seulement le quart inférieur droit de la texture et de le
redimensionner pour qu’il apparaisse dans un rectangle de 64 par 64 pixels.
protected override void Initialize()
{
sprite = new Sprite(new Rectangle(100, 100, 64, 64), new Rectangle(32, 32, 32,
➥ 32));
base.Initialize();
}
… ou encore la moitié haute du sprite sans la redimensionner.
=Labat FM.book Page 103 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
103

protected override void Initialize()


{
sprite = new Sprite(new Rectangle(100, 100, 64, 32), new Rectangle(0, 0, 64,
➥ 32));
base.Initialize();
}

Figure 6-7
Ici, seule la moitié haute de la texture est affichée

Passons à la quatrième surcharge. Comme vous pouvez le constater sur la figure 6-8, elle
est très similaire à la troisième surcharge, à la différence qu’elle prend un couple de coor-
données (Vector2) comme position de la texture à l’écran plutôt qu’un objet de type Rectangle.
Adaptez votre classe pour qu’elle utilise cette surcharge plutôt que la troisième. La fonc-
tionnalité de redimensionnement de la texture n’est pas disponible avec cette surcharge,
cependant vous la retrouverez dans la cinquième surcharge ou sous la forme de changement
d’échelle.

Figure 6-8
Le détail de la quatrième surcharge
=Labat FM.book Page 104 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


104

La classe Sprite qui prend en charge une position de type Vector2


class Sprite
{
Vector2 position;
public Vector2 Position
{
get { return position; }
set { position = value; }
}

Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}

Rectangle? sourceRectangle = null;


public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}

public Sprite(Vector2 position)


{
this.position = position;
}

public Sprite(Vector2 position, Rectangle? sourceRectangle)


{
this.position = position;
this.sourceRectangle = sourceRectangle;
}

public Sprite(float x, float y, Rectangle? sourceRectangle)


{
position = new Vector2(x, y);
this.sourceRectangle = sourceRectangle;
}

public void LoadContent(ContentManager content, string assetName)


{
texture = content.Load<Texture2D>(assetName);
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, Color.White);
}
}
=Labat FM.book Page 105 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
105

Cette nouvelle fonction de votre classe s’utilisera de la manière suivante.


protected override void Initialize()
{
sprite = new Sprite(new Vector2(100, 100), new Rectangle(0, 0, 64, 32));
base.Initialize();
}
Pour l’instant, conservez cette version de la classe. Nous allons continuer de la faire évoluer
au fur et à mesure du chapitre.

Faire défiler le décor : le scrolling


Nous allons maintenant découvrir une première utilisation de la sélection d’une portion
de texture. Il s’agit du scrolling. Derrière ce mot anglais se cache tout simplement la
notion de défilement de l’écran dans un jeu vidéo en deux dimensions. Cette technique
est utile lorsque l’intégralité du niveau ne peut être affichée sur un seul écran ; l’arrière-plan
se déplace alors suivant les mouvements du joueur.

Figure 6-9
Le jeu Super Tux utilise le scrolling

1. Pour commencer, rendez-vous sur le site du développeur et MVP (Microsoft Most


Valuable Professional) George Clingerman : http://www.xnadevelopment.com. Naviguez
ensuite jusqu’à la catégorie sprites et récupérez l’une des images d’arrière-plan qu’il
met gracieusement à la disposition des internautes. Vous pouvez aussi bien utiliser
une image de votre cru si vous le désirez.
=Labat FM.book Page 106 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


106

Figure 6-10
L’arrière-plan qui va être
utilisé

2. Ajoutez à votre projet les classes et interfaces nécessaires pour facilement récupérer
les entrées clavier de l’utilisateur, modifiez les espaces de noms puis initialisez-les.
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
3. L’arrière-plan que nous avons retenu pour illustrer le principe du défilement a une
résolution de 400 par 300 pixels. Redimensionnez donc la fenêtre de manière à ce
qu’elle n’affiche pas l’intégralité de l’image dans un seul écran. Utilisez par exemple
une résolution de 200 par 300 pixels, il s’agira donc de faire un scrolling horizontal.
public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferWidth = 200;
graphics.PreferredBackBufferHeight = 300;
}

4. Lors de l’initialisation de votre sprite, utilisez un rectangle qui commence aux coor-
données (0, 0) et qui correspond à la taille de l’écran.
sprite = new Sprite(new Vector2(0, 0), new Rectangle(0, 0, 200, 300));
Enfin, dans la méthode Update(), modifiez la coordonnée X de la position du rectan-
gle source. Augmentez-la si l’utilisateur appuie sur la flèche droite, diminuez-la dans
le cas contraire. Remarquez que cette coordonnée est en dehors de la taille réelle de
la texture, son extrémité (gauche ou droite selon les cas) est répétée à l’infini. Dans
le cas présent, il s’agit donc ici de la couleur bleu ciel, la même que l’arrière-plan.
Une classe de test de scrolling
public class ChapitreSix : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
=Labat FM.book Page 107 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
107

SpriteBatch spriteBatch;
Sprite sprite;

public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferWidth = 200;
graphics.PreferredBackBufferHeight = 300;
}

protected override void Initialize()


{
sprite = new Sprite(new Vector2(0, 0), new Rectangle(0, 0, 200, 300));
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
sprite.LoadContent(Content, "figure_6_11");
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Right))
sprite.SourceRectangle = new Rectangle(sprite.SourceRectangle.Value.X
➥ + gameTime.ElapsedGameTime.Milliseconds / 10, 0, 200, 300);
if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Left))
sprite.SourceRectangle = new Rectangle(sprite.SourceRectangle.Value.X
➥ - gameTime.ElapsedGameTime.Milliseconds / 10, 0, 200, 300);
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
=Labat FM.book Page 108 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


108

Figure 6-11
Le début d’un clone de Super Mario

Créer des animations avec les sprites sheets


Avez-vous déjà entendu parler de sprite sheet (feuille de sprites) ? Peut-être pas, mais
vous en avez sûrement déjà vu en fonctionnement. Il s’agit en fait d’une seule image sur
laquelle figure toute une déclinaison d’un ou plusieurs sprites à différents instants t.
L’ensemble des images d’un même sprite peut donc composer une animation.
Il est alors facile d’imaginer l’utilité des rectangles source dans ce cas de figure : déplacer
le rectangle d’un sprite à l’autre dès qu’un intervalle de temps est révolu.
Commencez par récupérer une planche de sprites ou bien, si vous avez des talents de
graphiste, faites-en une vous-même. Dans cette partie du chapitre, nous utiliserons à
nouveau une création de George Clingerman (http://www.xnadevelopment.com).

Figure 6-12
Une planche de sprite

Notre planche de sprite fait 600 par 100 pixels et comporte six états d’un sprite. Nos
rectangles source devront donc être des carrés de 100 pixels de côté.
1. Dans la classe Sprite, créez deux champs : l’un nommé index de type float et l’autre
nommé maxIndex de type int. Par défaut, ces deux variables doivent être initialisées à 0.
float index = 0;
int maxIndex = 0;
=Labat FM.book Page 109 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
109

2. Surchargez la méthode LoadContent() pour permettre de définir un index maximum


dans le cas d’une planche de sprite.
public void LoadContent(ContentManager content, string assetName, int maxIndex)
{
texture = content.Load<Texture2D>(assetName);
this.maxIndex = maxIndex;
}
3. Pour terminer, le traitement de l’animation va se faire dans la méthode Update(). À
chaque fois qu’elle est appelée, stockez le nombre de millisecondes écoulées depuis
le dernier appel dans la variable index. Si index à dépassé l’index maximum, fixez-le
à 0.
4. Enfin, modifiez l’objet sourceRectangle en changeant la position X par la valeur entière
de la variable index. Voici le nouveau code complet de la classe Sprite :
La classe Sprite avec la possibilité d’utiliser une feuille de sprites
class Sprite
{
Vector2 position;
public Vector2 Position
{
get { return position; }
set { position = value; }
}

Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}

Rectangle? sourceRectangle = null;


public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}

float index = 0;
int maxIndex = 0;

public Sprite(Vector2 position)


{
this.position = position;
}

public Sprite(Vector2 position, Rectangle? sourceRectangle)


{
this.position = position;
=Labat FM.book Page 110 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


110

this.sourceRectangle = sourceRectangle;
}

public Sprite(float x, float y, Rectangle? sourceRectangle)


{
position = new Vector2(x, y);
this.sourceRectangle = sourceRectangle;
}

public void LoadContent(ContentManager content, string assetName)


{
texture = content.Load<Texture2D>(assetName);
}

public void LoadContent(ContentManager content, string assetName, int maxIndex)


{
texture = content.Load<Texture2D>(assetName);
this.maxIndex = maxIndex;
}

public void Update(GameTime gameTime)


{
if (maxIndex != 0)
{
index += gameTime.ElapsedGameTime.Milliseconds * 0.001f;

if (index > maxIndex)


index = 0;

sourceRectangle = new Rectangle((int)index * sourceRectangle.Value.X,


➥ sourceRectangle.Value.Y, sourceRectangle.Value.Width,
➥ sourceRectangle.Value.Height);
}
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, Color.White);
}
}
5. De retour dans la classe ChapitreSix, il vous reste à modifier la taille du rectangle
dans l’initialisation du sprite, changer l’appel à la méthode LoadContent() et ajouter
l’appel de la méthode Update().
La classe de test des feuilles de sprites
public class ChapitreSix : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
=Labat FM.book Page 111 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
111

Sprite sprite;

public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferWidth = 100;
graphics.PreferredBackBufferHeight= 100;
}

protected override void Initialize()


{
sprite = new Sprite(new Vector2(0, 0), new Rectangle(0, 0, 100, 100));
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
sprite.LoadContent(Content, "figure_6_13", 6);
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
sprite.Update(gameTime);

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
=Labat FM.book Page 112 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


112

Varier la teinte des textures


Avant d’étudier la prochaine surcharge de la méthode Draw(), il est bon de revenir sur le
paramètre qui définit la teinte des textures. Jusqu’à présent, nous avons laissé ce paramè-
tre à la valeur Color.White. Il est temps d’essayer de faire varier cette valeur et d’observer
les résultats.
Reprenez la classe Sprite telle qu’elle était à la fin de la section « Texturer un objet
Rectangle », puis modifiez sa méthode Draw() afin de régler la teinte sur Color.Black et
exécutez l’application. Le résultat est visible sur la figure 6-13.
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, sourceRectangle, Color.Black);
}

Figure 6-13
Avec une teinte noire, la
texture semble… bien noire

Essayez la même chose mais cette fois-ci avec une teinte rouge.
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, sourceRectangle, Color.Red);
}
Vous pouvez créer des effets intéressants sur votre sprite grâce aux teintes. Pour cela,
ajoutez deux champs de type Color à votre classe Sprite. Le premier s’appelle color et
contient la teinte courante du sprite. Le second s’appelle nextColor et, comme son nom
l’indique, indique la prochaine teinte que votre sprite utilisera. L’effet que nous allons
réaliser ici devra faire varier doucement la teinte du sprite d’une couleur à l’autre. Ainsi,
le sprite s’illuminera puis s’assombrira.
Color color = Color.Gray;
Color nextColor = Color.White;
=Labat FM.book Page 113 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
113

L’évolution de la couleur de la teinte a lieu dans la méthode Update(). Chaque couleur


dispose de quatre composantes : rouge, vert, bleu et alpha. La composante alpha corres-
pond à la transparence. À chaque appel de la méthode, il faut faire évoluer chaque
composante (sauf la transparence qui n’a pas d’importance ici) vers la couleur objectif :
soit en l’augmentant, soit en la diminuant. Lorsque la couleur courante correspond à la
couleur objectif, on modifie cette dernière. N’oubliez pas de remplacer la couleur dans
l’appel à la méthode Draw() de l’objet spriteBatch.
La classe Sprite prend maintenant en charge la fonction de variation de sa teinte
class Sprite
{
Vector2 position;
public Vector2 Position
{
get { return position; }
set { position = value; }
}

Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}

Rectangle? sourceRectangle = null;


public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}

Color color = Color.Gray;


Color nextColor = Color.White;

public Sprite(Vector2 position, Rectangle? sourceRectangle)


{
this.position = position;
this.sourceRectangle = sourceRectangle;
}

public Sprite(float x, float y, Rectangle? sourceRectangle)


{
position = new Vector2(x, y);
this.sourceRectangle = sourceRectangle;
}

public void LoadContent(ContentManager content, string assetName)


{
texture = content.Load<Texture2D>(assetName);
=Labat FM.book Page 114 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


114

public void Update()


{
if (color.R < nextColor.R)
color.R++;
else if(color.R > nextColor.R)
color.R--;

if (color.G < nextColor.G)


color.G++;
else if (color.G > nextColor.G)
color.G--;

if (color.B < nextColor.B)


color.B++;
else if (color.B > nextColor.B)
color.B--;

if (color == Color.White)
nextColor = Color.Gray;
else if (color == Color.Gray)
nextColor = Color.White;
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, color);
}
}
Dans la classe ChapitreSix, n’oubliez pas d’ajouter l’appel à la méthode Update() de votre
objet sprite.
protected override void Update(GameTime gameTime)
{
sprite.Update();
base.Update(gameTime);
}
Les possibilités d’application de cet effet sont vastes : vous pouvez l’utiliser pour les
boutons de votre GUI, les menus de votre jeu ou encore un système de cycle jour/nuit
dans un jeu en deux dimensions.
Avant de continuer notre découverte des possibilités d’affichage des textures avec XNA,
adaptez votre classe pour qu’elle soit la plus générale possible : enlevez donc la variable
nextColor, ajoutez une propriété pour la variable color et surchargez le constructeur.
La classe Sprite plus générique que la précédente
class Sprite
{
Vector2 position;
=Labat FM.book Page 115 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
115

public Vector2 Position


{
get { return position; }
set { position = value; }
}

Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}

Rectangle? sourceRectangle = null;


public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}

Color color = Color.White;


public Color Color
{
get { return color; }
set { color = value; }
}

public Sprite(Vector2 position, Rectangle? sourceRectangle)


{
this.position = position;
this.sourceRectangle = sourceRectangle;
}

public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color)


{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
}

public void LoadContent(ContentManager content, string assetName)


{
texture = content.Load<Texture2D>(assetName);
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, color);
}
}
=Labat FM.book Page 116 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


116

Opérer des transformations sur un sprite


Les trois prochaines surcharges se ressemblent fortement. La seule différence entre elles
concerne la mise à l’échelle. Dans la cinquième, cela se fait via un rectangle de destina-
tion comme au début de ce chapitre, dans la sixième, cela se fait grâce à un coefficient de
type float et dans la dernière, cela se fait grâce à un Vector2. Le résultat est le même dans
les deux cas, c’est donc à vous de choisir celle qui convient le mieux à vos besoins.

Figure 6-14
Le détail des paramètres attendus par les trois dernières surcharges

Rotation
Tout d’abord la rotation. Celle-ci doit être exprimée en radian et s’effectue autour du
point d’origine.
Le point d’origine est le prochain paramètre à étudier. Pour que l’origine soit le coin
supérieur gauche de l’écran, le couple de valeurs doit être 0 et 0.

Bon à savoir
Vous n’êtes pas obligé de créer un vecteur pour certaines valeurs particulières. En effet, il existe deux
valeurs prédéfinies, l’une ayant ses composantes à 0 et l’autre ayant ses composantes à 1.

Le prochain paramètre attendu par la surcharge concerne l’échelle du sprite. Si vous lui
passez une valeur en inférieure à 1, il sera rétréci, dans le cas d’une valeur égale à 1, sa
taille ne sera pas modifiée et enfin, dans le cas d’une valeur supérieure à 1, il sera agrandi.
Vient ensuite le tour de l’énumération SpriteEffects. Celle-ci peut prendre trois valeurs :
None, FlipVertically et FlipHorizontally. None ne changera rien au rendu de votre image,
FlipVertically inversera l’image en la faisant tourner de 180° autour de l’axe horizontal
et, enfin, FlipHorizontally inversera l’image en la faisant tourner de 180° selon l’axe vertical.
Le dernier paramètre, layerDepth, est un nombre réel compris entre 0 et 1 déterminant
l’ordre de l’affichage des différents éléments. Une texture qui se voit attribuer un nombre
proche de 0 sera dessinée par-dessus une texture ayant un layerDepth proche de 1. Cepen-
dant, pour que ce paramètre rentre vraiment en compte, vous devez modifier un autre
élément lors de l’appel à la méthode Begin() de l’objet spriteBatch.
Testons à présent ces fonctionnalités une par une. La ligne suivante utilise la septième
surcharge de la fonction et dessine un sprite sans aucune modification particulière.
spriteBatch.Draw(texture, position, sourceRectangle, Color.White, 0, Vector2.Zero,
➥ Vector2.One, SpriteEffects.None, 0);
=Labat FM.book Page 117 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
117

Faisons nos premiers pas dans l’utilisation des rotations. Pour l’instant ne vous préoccupez
pas de l’origine.
1. Commencez par ajouter un champ de type float qui contiendra la valeur de la rotation
et ajoutez également une nouvelle surcharge du constructeur qui prendra en compte
cet élément.
float rotation = 0;
public float Rotation
{
get { return rotation; }
set { rotation = value; }
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, float rotation)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.rotation = rotation;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, sourceRectangle, Color.White, rotation,
➥ Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
2. Pour tester la rotation, créez trois sprites. Le premier ne subira aucune rotation, le
second une rotation de P/2, soit 90° dans le sens horaire et le dernier une rotation de
– P/2, soit 90° dans le sens anti-horaire.

Figure 6-15
Premier essai avec les rotations
=Labat FM.book Page 118 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


118

Sprite sprite;
Sprite sprite2;
Sprite sprite3;

public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
sprite = new Sprite(new Vector2(100, 100), null, 0);
sprite2 = new Sprite(new Vector2(100, 100), null, MathHelper.PiOver2);
sprite3 = new Sprite(new Vector2(100, 100), null, -MathHelper.PiOver2);
}
3. Procédez comme dans la première étape pour l’origine de la rotation. Commencez
par vous occuper de la classe Sprite.
Vector2 origin = Vector2.Zero;
public Vector2 Origin
{
get { return origin; }
set { origin = value; }
}

public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float


➥ rotation, Vector2 origin)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin,
➥ Vector2.One, SpriteEffects.None, 0);
}
4. Pour tester les rotations en modifiant l’origine, modifiez l’instanciation de votre
deuxième sprite. En prenant le point de coordonnées (32, 32), placez l’origine au milieu
de la texture : le sprite tournera donc sur lui-même.
sprite2 = new Sprite(new Vector2(100, 100), null, Color.White, MathHelper.PiOver2,
➥ new Vector2(32,32));
Le résultat de ce test est visible sur la figure 6-16.
=Labat FM.book Page 119 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
119

Figure 6-16
Rotations en modifiant
l’origine

Échelle
Progressons encore dans l’intégration de nouvelles fonctions à notre classe Sprite en
nous occupant cette fois-ci de l’échelle. Cette fonctionnalité peut être utilisée, par exem-
ple, pour redimensionner vos sprites en fonction de la résolution d’écran choisie par le
joueur.
Vector2 scale = Vector2.One;
public Vector2 Scale
{
get { return scale; }
set { scale = value; }
}

public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float


➥ rotation, Vector2 origin, Vector2 scale)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
this.scale = scale;
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin,
➥ scale, SpriteEffects.None, 0);
}
=Labat FM.book Page 120 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


120

Cette fois-ci, vous n’avez plus besoin que d’un seul et unique sprite pour faire ce test.
Dans l’exemple ci-dessous, le sprite est volontairement disproportionné.
sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero, new
➥ Vector2(0.5f, 4));

Figure 6-17
Modification de l’échelle
du sprite

Inversion
Procédez toujours de la même manière pour le prochain paramètre. Grâce à lui, si vous
disposez d’une texture représentant la marche d’un personnage de la gauche vers la
droite, vous pourrez l’inverser selon l’axe vertical et ainsi obtenir la marche de la droite
vers la gauche.
SpriteEffects effect = SpriteEffects.None;
public SpriteEffects Effect
{
get { return effect; }
set { effect = value; }
}

public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float


➥ rotation, Vector2 origin, Vector2 scale, SpriteEffects effect)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
this.scale = scale;
=Labat FM.book Page 121 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
121

this.effect = effect;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin,
➥ scale, effect, 0);
}
Dans cet exemple, l’inversion se fait selon l’axe horizontal : le haut de la texture va se
retrouver en bas.
sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero,
➥ Vector2.One, SpriteEffects.FlipVertically);
Enfin, il ne reste plus qu’à ajouter la définition de la profondeur. C’est cette notion qui
définira l’ordre d’affichage des différents sprites à l’écran. Ci-dessous, vous retrouvez le
code complet de la classe Sprite qui possède maintenant de puissantes fonctions avancées.
La classe Sprite qui prend en compte toutes les techniques vues dans ce chapitre
class Sprite
{
Vector2 position;
public Vector2 Position
{
get { return position; }
set { position = value; }
}
Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}
Rectangle? sourceRectangle = null;
public Rectangle? SourceRectangle
{
get { return sourceRectangle; }
set { sourceRectangle = value; }
}
Color color = Color.White;
public Color Color
{
get { return color; }
set { color = value; }
}
float rotation = 0;
public float Rotation
{
get { return rotation; }
set { rotation = value; }
}
=Labat FM.book Page 122 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


122

Vector2 origin = Vector2.Zero;


public Vector2 Origin
{
get { return origin; }
set { origin = value; }
}
Vector2 scale = Vector2.One;
public Vector2 Scale
{
get { return scale; }
set { scale = value; }
}
SpriteEffects effect = SpriteEffects.None;
public SpriteEffects Effect
{
get { return effect; }
set { effect = value; }
}
float layerDepth = 0;
public float LayerDepth
{
get { return layerDepth; }
set { layerDepth = value; }
}
public Sprite(Vector2 position)
{
this.position = position;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float
➥ rotation)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float
➥ rotation, Vector2 origin)
=Labat FM.book Page 123 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
123

{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float
➥ rotation, Vector2 origin, Vector2 scale)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
this.scale = scale;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float
➥ rotation, Vector2 origin, Vector2 scale, SpriteEffects effect)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
this.scale = scale;
this.effect = effect;
}
public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float
➥ rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth)
{
this.position = position;
this.sourceRectangle = sourceRectangle;
this.color = color;
this.rotation = rotation;
this.origin = origin;
this.scale = scale;
this.effect = effect;
this.layerDepth = layerDepth;
}
public void LoadContent(ContentManager content, string assetName)
{
texture = content.Load<Texture2D>(assetName);
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, sourceRectangle, color, rotation,
➥ origin, scale, effect, layerDepth);
}
}
=Labat FM.book Page 124 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


124

Pour tester cette dernière fonctionnalité, vous aurez besoin de deux sprites. L’un aura une
valeur de profondeur de 1 et l’autre 0.
sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero,
➥ Vector2.One, SpriteEffects.FlipVertically, 0);
sprite2 = new Sprite(new Vector2(140, 140), null, Color.White, 0, Vector2.Zero,
➥ Vector2.One, SpriteEffects.None, 1);
Cependant, ce code n’est pas encore opérationnel. Essayez les deux versions de la méthode
Draw() ci-dessous.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
sprite.Draw(spriteBatch);
sprite2.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
sprite2.Draw(spriteBatch);
sprite.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
Vous constaterez que les valeurs de profondeur que vous avez fixées pour chacun des
deux sprites ne sont pas appliquées. Dans la première méthode Draw(), c’est le deuxième
sprite qui chevauche le premier alors que dans la seconde version de la méthode, c’est le
premier qui chevauche le deuxième.
Il faut paramétrer votre objet spriteBatch pour que cette fonctionnalité soit effective.
Cela se fait par l’intermédiaire de la méthode Begin().
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.BackToFront,
➥ SaveStateMode.None);
Le premier paramètre concerne la transparence. Le second, celui qui vous intéresse, indi-
quera l’ordre d’affichage des sprites. Il peut prendre comme valeurs SpriteSortMode
.BackToFront (affichage des sprites les plus profonds d’abord) ou SpriteSortMode.FrontToBack
(affichage des sprites en partant du premier plan). Enfin, le dernier paramètre permet
d’enregistrer l’état du périphérique graphique, il ne sera pas utilisé ici.
=Labat FM.book Page 125 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
125

À présent, vous constatez que quel que soit l’ordre des lignes d’appel de la méthode
Draw() des deux sprites, c’est le premier sprite qui est situé au-dessus du second.

Figure 6-18
L’ordre d’affichage
des sprites est maintenant
respecté

Afficher du texte avec Spritefont


En lecteur curieux, vous vous êtes certainement rendu compte que la classe SpriteBatch
dispose d’une méthode DrawString(). Celle-ci prend comme paramètre un objet de type
SpriteFont, le texte à afficher, un objet Vector2 correspondant à la position du coin supérieur
gauche de la chaîne et, enfin, la couleur de la chaîne de caractères. Un objet SpriteFont
sert à indiquer quelle texture utiliser pour dessiner du texte.

Figure 6-19
Il existe une méthode qui permet d’afficher simplement du texte

Pour créer un SpriteFont, commencez par ajouter un fichier dédié au gestionnaire de


contenu (figure 6-20). Ouvrez ensuite ce nouveau fichier, qui est en fait un fichier XML.
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Kootenay</FontName>
<Size>14</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
=Labat FM.book Page 126 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


126

<Style>Regular</Style>
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

Figure 6-20
Ajout d’un SpriteFont au projet

À partir de ce fichier, définissez la police de caractères que vous souhaitez utiliser, sa taille,
l’espace entre les caractères, le style ou encore les caractères qui seront disponibles.

En pratique
La police de caractères que vous voulez employer doit se situer dans le répertoire Fonts de Windows. Le
Content Manager la transformera ensuite en fichier .xnb afin de pouvoir l’utiliser avec votre projet.

La portion de code ci-dessous montre que modifier et adapter une police à ses besoins est
réellement intuitif avec XNA. Ici, nous modifions la police utilisée en changeant simplement
le nom situé entre les balises <FontName>, ainsi que la taille de la police en modifiant le
nombre placé entre les balises <Size>.
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
=Labat FM.book Page 127 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
127

<FontName>Verdana</FontName>
<Size>26</Size>
<Spacing>10</Spacing>
<UseKerning>true</UseKerning>
<Style>Bold</Style>
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
Vous n’avez plus qu’à charger la police en déclarant un objet de type SpriteFont et enfin
compléter la méthode DrawString().
Classe de test du SpriteFont
public class ChapitreSix : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("font");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
=Labat FM.book Page 128 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


128

spriteBatch.Begin();
spriteBatch.DrawString(font, "test", new Vector2(0, 0), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}

Afficher le nombre de FPS


Le nombre de FPS (Frames Per Second), c’est-à-dire la quantité d’images affichées par
seconde, deviendra vite une de vos plus grandes obsessions. En effet, plus ce nombre est
élevé, plus une animation semble fluide. Si vous effectuez un grand nombre de calculs
qui ralentissent l’apparition de chaque image, votre jeu risque de donner une impression
de saccades.
L’objectif de votre jeu devrait être d’avoisiner les 60 FPS. Par défaut, XNA bridera votre
jeu pour qu’il ne dépasse pas cette limite. Vous pouvez considérer que 30 FPS est également
acceptable, cependant, évitez de descendre en dessous de ce nombre. Pour information,
au cinéma, la norme est de 24 images par secondes.
La mesure du nombre de FPS permet de rapidement juger des performances de votre jeu.
Cependant, cela ne pourra pas vraiment vous aider pour optimiser votre code ou comparer
la vitesse de différents algorithmes.
Le composant que vous allez maintenant développer vous permettra d’afficher à l’écran
le nombre courant de FPS. Ajoutez une nouvelle classe au projet et faites-la dériver de
DrawableGameComponent. Préparez un objet de type SpriteBatch et un autre de type SpriteFont.
Il existe de nombreuses méthodes pour calculer le nombre d’images par seconde. La techni-
que utilisée ici est celle proposée par Shawn Hargreaves sur son blog (http://blogs.msdn.com/
shawnhar/). Elle consiste à compter le nombre de passages dans la méthode Draw() puis, à
chaque seconde, en déduire le nombre de FPS. La classe TimeSpan est accessible dans
l’espace de noms System, n’oubliez pas de l’ajouter !

Composant d’affichage du nombre de FPS


class FPSComponent : DrawableGameComponent
{
SpriteBatch spriteBatch;
SpriteFont spriteFont;
int frameRate = 0;
int frameCounter = 0;
TimeSpan elapsedTime = TimeSpan.Zero;
public FPSComponent(Game game)
: base(game)
{
}
=Labat FM.book Page 129 Vendredi, 19. juin 2009 4:01 16

Enrichir les sprites : textures, défilement, transformation, animation


CHAPITRE 6
129

public override void Initialize()


{
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
spriteFont = Game.Content.Load<SpriteFont>("font");
}
protected override void LoadContent()
{
}
public override void Update(GameTime gameTime)
{
elapsedTime += gameTime.ElapsedGameTime;
if (elapsedTime > TimeSpan.FromSeconds(1))
{
elapsedTime -= TimeSpan.FromSeconds(1);
frameRate = frameCounter;
frameCounter = 0;
}
}
public override void Draw(GameTime gameTime)
{
frameCounter++;
spriteBatch.Begin();
spriteBatch.DrawString(spriteFont, frameRate.ToString() + " FPS", new
➥ Vector2(0, 0), Color.White);
spriteBatch.End();
}
}

Pour l’utiliser, vous n’avez plus qu’à l’ajouter à la liste des composants.
public ChapitreSix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
Components.Add(new FPSComponent(this));
Avant de terminer ce chapitre, un dernier détail concernant l’optimisation de la taille des
polices de caractère. Pour celle chargée d’afficher le nombre de FPS, Shawn Hargreaves
propose de ne spécifier dans le fichier .spritefont que les caractères qui seront utilisés.
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Arial</FontName>
<Size>14</Size>
<Spacing>2</Spacing>
<Style>Regular</Style>
=Labat FM.book Page 130 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


130

<CharacterRegions>
<CharacterRegion>
<Start>F</Start>
<End>F</End>
</CharacterRegion>
<CharacterRegion>
<Start>P</Start>
<End>P</End>
</CharacterRegion>
<CharacterRegion>
<Start>S</Start>
<End>S</End>
</CharacterRegion>
<CharacterRegion>
<Start> </Start>
<End> </End>
</CharacterRegion>
<CharacterRegion>
<Start>0</Start>
<End>9</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

En résumé
Dans ce chapitre vous avez d’abord découvert plusieurs techniques pour enrichir l’affichage
de vos sprites :
• n’en sélectionner qu’une portion ;
• modifier leur teinte ;
• effectuer des rotations ;
• modifier leur échelle ;
• les inverser selon un axe ;
• modifier leur ordre d’affichage.
Vous avez également appris à afficher du texte et appliqué cette technique pour créer un
composant qui indique le nombre de FPS.
=Labat FM.book Page 131 Vendredi, 19. juin 2009 4:01 16

7
La sonorisation

Ce chapitre porte sur un élément fondamental d’un bon jeu : le son. En effet, un bon environ-
nement sonore est indispensable pour donner de la profondeur et du réalisme à votre jeu
et favoriser l’immersion du joueur, qu’il s’agisse de la musique ou des bruitages. Pour
pousser la logique à son extrême, sachez qu’il existe même des jeux qui ne comportent
que du son et aucun élément graphique !
Jusqu’à la version 2 de XNA, le son était géré par l’API XACT, directement tirée de
DirectX. Depuis l’arrivée de XNA 3.0, les développeurs ont accès à une nouvelle API
pour gérer le son. Cette dernière est plus simple d’utilisation (vous pouvez manier les
éléments sonores comme des textures, des SpriteFont, etc.), mais ne vous offrira pas des
fonctions aussi avancées que XACT. Pour que vous soyez également en mesure de
comprendre le code source d’un jeu écrit pour XNA 2.0, nous couvrirons ces deux
méthodes.

Travailler avec XACT


Aussi appelé Microsoft Cross-Platform Audio Creation Tool, XACT est la partie du
framework qui sert à créer des projets audio aussi bien pour la Xbox 360 que pour
Windows. Comme nous l’avons précisé en introduction de ce chapitre, jusqu’à l’arrivée
de la version 3.0 du framework, c’était la seule solution pour gérer le son dans XNA.
Mais ce n’est pas parce qu’une nouvelle API est arrivée dans le framework que XACT est
un mauvais outil ! Au contraire, il est inclus avec le framework pour permettre d’utiliser
les mêmes pistes son et ne pas avoir à redévelopper votre jeu lors du portage entre les
différentes plates-formes couvertes par XNA.
=Labat FM.book Page 132 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


132

Zune
L’utilisation de XACT est impossible pour un projet de jeu Zune.

Créer un projet sonore


Commencez par démarrer le logiciel, qui se trouve au même endroit que les autres utilitaires
livrés avec XNA, situés par défaut dans le dossier C:\Program Files\Microsoft XNA\XNA Game
Studio\v3.0\Tools, ou via le menu Démarrer :
1. Cliquez sur Démarrer.
2. Cliquez sur Tous les programmes.
3. Dans Microsoft XNA Game Studio 3.0 Tools, sélectionnez Microsoft Cross-Platform
Audio Creation Tool (XACT).

Figure 7-1
L’interface de XACT

L’interface de l’utilitaire se divise en quatre zones. De haut en bas, nous trouvons :


• La barre des menus qui sert à gérer les projets et l’apparence du logiciel.
• L’explorateur du projet grâce auquel vous inspecterez sous forme d’arborescence tous
les éléments du projet.
• L’explorateur de propriétés où vous pourrez modifier les paramètres de tous les éléments
du projet.
• La zone centrale qui, bien évidemment, vous permettra de travailler sur les éléments
du projet.
=Labat FM.book Page 133 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
133

En pratique
L’agencement des différentes parties de l’utilitaire est très similaire à l’agencement par défaut dans Visual
Studio.

Voyons comment créer un projet sonore :


1. Créez un nouveau projet via le menu File puis New Project. La plupart des éléments
de l’interface sont maintenant actifs. Notez que XACT génère deux fichiers de para-
métrage du projet : un pour la Xbox 360 et l’autre pour Windows (figure 7-2).

Figure 7-2
Un fichier de configuration
par plate-forme

Avant de continuer, il faut connaître un peu la terminologie liée à l’organisation logique


des éléments sonores d’un projet XACT.

Tableau 7-1 Signification des éléments d’un projet XACT

Nom Description
Wave Il s’agit d’un fichier audio.
Wave Bank Il s’agit du regroupement logique dans un seul fichier de plusieurs fichiers audio.
Cue Permet de jouer des sons.
Sound Bank C’est le regroupement logique de plusieurs Wave Bank et de Cue.

2. Ajoutez une nouvelle banque de sons (Wave Bank). Pour cela, deux solutions s’offrent
à vous : via le menu Wave Banks>New Wave Bank, ou bien par l’explorateur de
projets grâce à un clic droit sur Wave Banks, puis un clic sur New Wave Bank. Vous
pouvez maintenant consulter les propriétés de votre banque de sons. Vous y retrouvez
le nom de la banque, une description, son type, sa taille et la méthode de compression
utilisée.
3. Ensuite, ajoutez un fichier à la banque : cliquez droit dans la zone de travail puis sur
Insert Wave File(s). Les formats supportés sont .wav, .aif, .aiff. Vous voyez à présent
diverses informations sur les pistes, notamment un détail de la compression disponible
dans l’explorateur de propriétés.
Le format .wav est un format de stockage audio défini par Microsoft et IBM. Il peut contenir
des données aux formats MP3, WMA, etc. Les formats .aif et .aiff sont équivalents au
.wav, mais sont développés par Apple.
Si vous avez de la musique dans votre jeu, déclarez-la telle quelle : déplacez-la dans la
catégorie Music de l’arborescence.
=Labat FM.book Page 134 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


134

Figure 7-3
Ajout d’une banque de sons

Figure 7-4
La banque de sons contient maintenant une piste
=Labat FM.book Page 135 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
135

Écouter un son
Si vous voulez écouter une piste sonore à partir de XACT, vous devrez d’abord lancer l’utilitaire XACT
Auditioning Utility disponible au même endroit.

4. Créez ensuite une Sound Bank (via le menu Sound Banks, puis New Sound Bank).
Dans la fenêtre qui s’ouvre alors, vous distinguez deux zones : celle du haut contient
les pistes audio et celle du bas rassemble les pistes Cue correspondantes. Pour ajouter
une piste à la Sound Bank, glissez-déposez la piste vers la deuxième zone.

Figure 7-5
Ajout d’une piste à la Sound Bank

5. À présent, le projet est prêt à être généré : cliquez sur File, puis sur Build, ou utilisez
le raccourci clavier F7. Les fichiers générés sont disponibles dans les sous-dossiers
Win ou Xbox, à l’emplacement où vous avez sauvegardé votre projet XACT.

Lire les fichiers créés


Votre projet a été généré, il ne reste plus qu’à l’importer dans Visual Studio pour l’utiliser.
De retour dans Visual Studio, commencez par créer un nouveau projet afin de tester les
possibilités sonores de XNA. L’organisation logique que nous avons créée précédemment
=Labat FM.book Page 136 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


136

se charge aussi simplement qu’une texture. En effet, il suffit d’ajouter le fichier .xap du
projet XACT dans le Content Manager, qui importe automatiquement le projet vers le
jeu. Cependant, vous ne devez pas vous contenter d’ajouter ce fichier au projet, il faut
aussi ajouter toutes les pistes audio à utiliser dans le projet.

En pratique
Dans le cas d’un projet de jeu complet, cette organisation logique est beaucoup plus complexe : vous
devrez en effet gérer de nombreuses banques de sons (pour les bruitages d’un personnage ou de
l’environnement, les musiques, etc.).

Figure 7-6
Voila à quoi devrait ressembler
le projet

Il est enfin temps de passer à la programmation d’une classe test du projet sonore. Créez
des objets de type AudioEngine, SoundBank et WaveBank en chargeant les fichiers générés
par XACT. Attention, vous devez spécifier le chemin complet vers ces fichiers ! N’oubliez
pas non plus les extensions de fichiers.
Vous pouvez ensuite lire une piste Cue simplement en utilisant la méthode PlayCue () de
l’objet de type SoundBank. N’oubliez pas d’appeler auparavant la méthode Update() de l’objet
AudioEngine pour le mettre à jour. Celle-ci devra également être appelée dans la méthode
Update() de la classe principale.
Classe test pour la lecture d’une piste Cue
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
AudioEngine engine;
WaveBank waveBank;
SoundBank soundBank;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
engine = new AudioEngine(@"Content\test.xgs");
soundBank = new SoundBank(engine, @"Content\Sound Bank.xsb");
waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb");
}
=Labat FM.book Page 137 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
137

protected override void Initialize()


{
engine.Update();
soundBank.PlayCue("Explosion1");
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
engine.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
Exécutez le programme. Le son est lu sans problème. Charger et lire un son est finalement
aussi simple que pour les textures !
Vous auriez également pu stocker la piste dans un objet de type Cue et récupérer celui-ci
grâce à la méthode GetCue du SoundBank. La piste se lit ensuite grâce à la méthode Play() :
Classe test d’utilisation d’un objet de type Cue
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
AudioEngine engine;
WaveBank waveBank;
SoundBank soundBank;
Cue sound;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
engine = new AudioEngine(@"Content\test.xgs");
soundBank = new SoundBank(engine, @"Content\Sound Bank.xsb");
waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb");
}
=Labat FM.book Page 138 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


138

protected override void Initialize()


{
engine.Update();
sound = soundBank.GetCue("Explosion1");
sound.Play();
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
engine.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}

Lire les fichiers en streaming


La méthode que nous venons de voir charge les sons en mémoire. Cette solution est peu
recommandée dès que le projet dispose d’un certain nombre de pistes, surtout si elles ne
sont pas compressées. La seconde méthode que nous allons détailler permet de lire les
données en streaming, c’est-à-dire en chargement continu.
1. Ouvrez de nouveau XACT et le projet de la première méthode.
2. Cependant, cette fois-ci, sélectionnez Streaming dans les propriétés de la Wave Bank.
Figure 7-7
Sélection du mode streaming

3. Reconstruisez le projet (F7).


4. Au niveau du code du projet, vous devez seulement appeler un autre constructeur
pour la classe WaveBank.
waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb", 0, 2);
=Labat FM.book Page 139 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
139

Figure 7-8
Le nouveau constructeur de la classe WaveBank

5. Le premier des deux nouveaux paramètres est l’offset de démarrage de la ressource


Wave Bank. Ce paramètre est utile si vous lisez un son depuis un DVD, sinon laissez-
le à 0. Le second paramètre est la taille du buffer utilisé pour le streaming. Vous
pouvez considérer qu’il s’agit de la valeur qui déterminera la qualité du son. La valeur
minimale est de 2 et il n’est pas conseillé de dépasser 16, faute de quoi le son risque
de ne plus être diffusé de manière ininterrompue.

Compression
Si vous utilisez le mode In Memory, vous préférerez sûrement compresser les pistes pour
prendre le moins de place possible en mémoire. Retournez au projet dans XACT. Dans
l’explorateur de projet, faites un clic droit sur Compression Presets, puis sur New
Compression Preset.

Figure 7-9
Propriété d’un nouveau
preset de compression

La compression n’est pas la même pour la Xbox que pour Windows. Dans les deux cas,
PCM signifie que le fichier ne sera pas compressé.
Tableau 7-2 Les méthodes de compression

Compression Description
XMA (Xbox 360) Vous devez spécifier la qualité de la piste via un curseur. Plus la valeur est faible, plus la qualité
est faible, mais plus la compression est impor tante. Par défaut, la valeur est de 60.
ADPCM Vous devez spécifier le nombre d’échantillons par bloc. Plus le nombre est grand, plus la
(Windows) qualité est satisfaisante, mais moins la compression est bonne. Par défaut, la valeur est de
128 échantillons par bloc.
=Labat FM.book Page 140 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


140

Une fois que vous avez créé un preset, vous pouvez l’appliquer soit à une piste, soit à un
Wave Bank. Vous n’avez plus qu’à reconstruire le projet.

Figure 7-10
Application d’un preset à
une banque de sons

Ajouter un effet de réverbération


L’effet de « réverb », bien connu des musiciens, vise à donner l’impression d’être dans un
lieu plus ou moins vaste. Il peut être ajouté aux pistes via XACT. Dans l’explorateur de
projet, ces effets sont regroupés sous le nom « DSP Effect Path Presets ».
Voici comment ajouter un nouvel effet :
1 Dans XACT, cliquez avec le bouton droit sur la catégorie DSP Effect Path Presets de
l’explorateur de projet, puis sélectionnez New Microsoft Reverb Project.

Figure 7-11
Le paramétrage de la réver-
bération peut être très
poussé

2. À partir de là, vous pouvez régler très précisément les paramètres de la réverbération
(figure 7-11). Vous pouvez aussi choisir d’utiliser les paramètres préréglés disponi-
bles dans la liste déroulante Effect Preset, qui permettent d’obtenir facilement la
réverbération d’une cave, d’une salle de concert, d’un hangar, etc.
=Labat FM.book Page 141 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
141

3. Reste à appliquer l’effet à une piste. Cliquez avec le bouton droit sur le nom de l’effet
dans l’explorateur de projet, puis cliquez sur Attach/Detach Sound(s)… À partir de
la fenêtre qui s’est ouverte, attachez l’effet à chacune des pistes ou détachez-le.
Figure 7-12
La fenêtre de liaison pistes/
effet

4. Pour rendre ces modifications utilisables dans le jeu, reconstruisez le projet (F7) ;
aucune autre modification n’est nécessaire sous Visual Studio.

Le son avec la nouvelle API SoundEffect


Vous venez de voir que XACT est très simple d’utilisation. Cependant, les développeurs
de XNA ont eu des remontées de nombreuses personnes qui trouvaient son utilisation un
peu lourde pour des petits projets, notamment à cause de l’utilisation d’un outil externe
et de la gestion de l’arborescence de la banque de sons.
Les développeurs ont donc ajouté une nouvelle API, baptisée SoundEffect, pour la
gestion du son. Celle-ci rend le chargement et l’utilisation de pistes sonores aussi simples
que dans le cas d’une texture, sans que vous ayez à gérer tout un projet sonore comme
vous le feriez pour XACT. De plus, si vous avez l’intention de porter le projet pour Zune,
sachez que seule cette nouvelle API est disponible en ce qui concerne le son.

Lire un son
La lecture d’un son se fera en utilisant un objet de type SoundEffect. Ajoutez simplement un
fichier .wav au projet comme vous ajouteriez une texture (voir le chapitre 6), puis créez
l’objet SoundEffect et chargez le son. La lecture se fait ensuite grâce à la méthode Play().
Test de l’API SoundEffect
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SoundEffect soundEffect;
=Labat FM.book Page 142 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


142

public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}

protected override void Initialize()


{
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
soundEffect = Content.Load<SoundEffect>("Explosion1");
soundEffect.Play();
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}

Lire un morceau de musique


Grâce à la classe MediaLibrary, vous accédez à la musique présente dans la bibliothèque
multimédia de l’utilisateur. Ces musiques doivent avoir été détectées au préalable par
Windows Media Player. Vous pourrez ensuite jouer un morceau d’un disque présent dans
cette bibliothèque multimédia grâce à la classe MediaPlayer.
Le code ci-dessous lit la première piste d’un album choisi au hasard dans la bibliothèque
du joueur. Si ce dernier appuie sur la touche Espace et que la piste est en cours de lecture,
elle est mise en pause. Si elle est déjà en pause, la lecture reprend.
Test de classe MediaPlayer
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
=Labat FM.book Page 143 Vendredi, 19. juin 2009 4:01 16

La sonorisation
CHAPITRE 7
143

SpriteBatch spriteBatch;

MediaLibrary sampleMediaLibrary;
Random rand;

public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

sampleMediaLibrary = new MediaLibrary();


rand = new Random();
}

protected override void Initialize()


{
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
int i = rand.Next(0, sampleMediaLibrary.Albums.Count - 1);
MediaPlayer.Play(sampleMediaLibrary.Albums[i].Songs[0]);
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Space))
{
if (MediaPlayer.State == MediaState.Playing)
MediaPlayer.Pause();
else
MediaPlayer.Resume();
}

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
=Labat FM.book Page 144 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


144

Pour un bon design sonore


À première vue, le son n’est pas l’élément qui vous causera le plus de soucis lors de la
création d’un jeu vidéo : il suffit juste de charger les sons et de les jouer. Cependant, les
joueurs n’ont pas tous les mêmes équipements audio !
De plus, certains joueurs n’aiment pas les musiques présentes dans les jeux et préfèrent
écouter les leurs… Veillez donc à leur laisser la possibilité de les éteindre sans pour
autant couper tous les bruitages, par exemple par l’intermédiaire d’un menu d’options.
Ci-dessous vous retrouverez quelques exemples de la liste des actions conseillées par le
site du Creators Club pour le son des jeux de la Xbox 360 (la liste complète est disponible
sur http://creators.xna.com/en-US/education/bestpractices). Certains de ces conseils s’appliquent
aussi aux jeux développés pour les autres plates-formes.
• Essayez de tester la partie sonore du jeu sur le plus grand nombre de configurations
possibles (stéréo, mono, casques, etc.).
• Assurez-vous, au moment de leur édition, que le volume des musiques et/ou des bruitages
est normalisé afin que les joueurs n’aient pas à le modifier au cours du jeu. Pour définir
cette norme, basez-vous sur le volume du son de démarrage de la Xbox 360 : réglez-le
pour qu’il soit un peu fort et ajustez ensuite le volume du jeu en conséquence.
• Faites attention : sur la Xbox 360, la méthode Play() de la classe MediaPlayer est
asynchrone, ce qui signifie que les musiques ne sont pas immédiatement lues. Si vous
vérifiez l’état de l’objet MediaPlayer dans l’appel à la méthode Update() qui suit la
lecture de la musique, il sera probablement toujours sur MediaState.Stopped. Ainsi, si
vous voulez démarrer une nouvelle musique à la fin de la lecture de l’ancienne, n’utilisez
pas la méthode précédente, mais attendez plutôt l’événement ActiveSongChanged puis,
seulement à partir de ce moment-là, vérifiez l’état du MediaPlayer.
• Par défaut, le gestionnaire de contenu (le Content Processor pour être plus précis)
compresse au maximum les éléments qui seront utilisés par l’API SoundEffect. Songez-y
et souvenez-vous que la compression améliore la taille du jeu au détriment de la
qualité du son.

En résumé
Dans ce chapitre, vous avez découvert :
• comment gérer un projet sonore avec XACT et utiliser ce projet dans XNA ;
• quels sont les avantages et inconvénients de l’API SoundEffect par rapport à XACT et
comment utiliser cette API dans XNA ;
• comment utiliser la classe MediaPlayer pour jouer des musiques de la bibliothèque
multimédia de l’utilisateur ;
• quelques bonnes pratiques à appliquer dans le design sonore d’un jeu.
=Labat FM.book Page 145 Vendredi, 19. juin 2009 4:01 16

8
Exceptions et gestion
des fichiers : sauvegarder
et charger un niveau

Vous allez sûrement vouloir charger des niveaux et en ajouter facilement de nouveaux aux
jeux. Vous voudrez aussi probablement que l’expérience de jeu qu’auront les utilisateurs
s’inscrive dans la durée en enregistrant leur progression ou leur score.
Ce chapitre apporte toutes les réponses à vos questions en ce qui concerne le stockage de
données sur les périphériques physiques (disque dur, carte mémoire, etc.). Il commence
par une partie théorique sur les emplacements de stockage offerts par XNA, les fichiers
XML et la sérialisation. Vous découvrirez ensuite les Gamer Services. Enfin, vous passerez
à la pratique en apprenant à gérer et à utiliser fichiers et répertoires.

Le stockage des données


Dans cette première partie, vous allez découvrir la théorie liée aux différents emplacements
de stockage mis à disposition par XNA, ainsi qu’aux différentes méthodes de sauvegarde de
données.

Les espaces de stockage


Le framework XNA met à votre disposition deux espaces de stockage bien distincts :
• Le dossier du jeu dans lequel se situe l’exécutable, ainsi que tout le contenu que vous
aurez créé (textures, sons, etc.).
=Labat FM.book Page 146 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


146

Zune
Sur le Zune, cet espace de stockage se limite à 16 Mo. Cela correspond à la mémoire vive qu’un jeu peut
utiliser.

• Le dossier de l’utilisateur où se situent les sauvegardes ou la configuration préférée


d’un joueur.
Sous Windows, il s’agit du répertoire SavedGames situé dans le répertoire personnel
de l’utilisateur connecté sur le PC. À l’intérieur de ce répertoire se trouve un dossier
correspondant au nom du jeu exécuté. Enfin, la dernière ramification correspond au
numéro du joueur (PlayerIndex). Si aucun n’est spécifié, les fichiers se trouvent dans
le répertoire AllPlayers, sinon dans un répertoire correspondant à ce numéro (Player1,
Player2, Player3 ou Player4).
Sur Xbox, il s’agit du disque dur ou d’une carte mémoire : c’est le joueur qui
choisira le périphérique de stockage qu’il souhaite utiliser.

Figure 8-1
Fichier personnel de configuration du jeu Racing Game

Lecteur réseau
Si votre dossier personnel (Documents and Settings\<Utilisateur>) ne se situe pas sur l’ordinateur
où vous exécuterez un jeu qui doit sauvegarder des données personnelles, mais sur un lecteur réseau,
une exception sera levée rendant la sauvegarde impossible. À l’heure actuelle, l’exécution d’un jeu à
travers le réseau n’est pas supportée par XNA.

Le format XML, un format intelligible qui simplifie le stockage de données XML (eXtensible
Markup Language, en français langage extensible de balisage) est utilisé pour contenir
des données qui seront encadrées par des balises. Si vous vous êtes déjà aventuré dans la
création d’un site web, vous avez probablement rencontré le HTML qui repose également
sur des balises utilisées pour formater l’affichage de données.
L’autre spécificité de XML est qu’il n’y a pas une liste de balises définies : c’est à vous
de créer celles qui vous seront utiles.
Ainsi, XML est utilisé dans des domaines très variés :
• Les fichiers de configuration de logiciels ou de jeux sont de plus en plus basés sur ce
format. Certains utilisent même ces fichiers pour la configuration de l’interface utilisateur,
permettant ainsi aux utilisateurs novices de la paramétrer très facilement.
=Labat FM.book Page 147 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
147

• Dans les jeux vidéos, les fichiers de sauvegarde ou ceux qui décrivent les niveaux
peuvent aussi utiliser le format XML.
Ci-dessous, vous retrouvez un fichier de configuration fictif, utilisant le XML, qui pourrait
correspondre à la résolution de l’écran dans un de vos jeux.
<?xml version="1.0" encoding="ISO-8859-1"?>
<GameConfiguration> 
<ScreenSize width="800" height="600" /> 
</GameConfiguration >

Figure 8-2
Un fichier XML ouvert dans Internet Explorer 7

Cependant, beaucoup d’informaticiens ne se sont pas encore ralliés à la cause de XML.


Certains préféreront utiliser des formats utilisant des séparateurs de données. L’exemple
qui suit correspond au fichier de configuration fictif de l’exemple précédent, mais cette
fois-ci dans le format CSV (Comma Separated Values, valeurs séparées par des virgules).
800,600
D’autres préféreront les fichiers INI, pourtant progressivement abandonnés par Microsoft
depuis Windows 95. Dans ce genre de fichier, vous définissez des sections puis vous
=Labat FM.book Page 148 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


148

affectez des valeurs à différentes variables. Ci-dessous, la configuration de la résolution


de l’écran au format INI.
[ScreenSize]
Width=800
Height=600
Libre à vous d’utiliser le format que vous préférez. Vous pouvez même en inventer un, ou
utiliser des fichiers binaires (qui ne pourront pas être lus directement par un éditeur de
texte). Retenez cependant que XML est un langage très simple à utiliser et très générique.

Sérialisation et désérialisation
La sérialisation (en anglais serialization) est un processus qui permet d’enregistrer l’état
complet d’un objet à un moment donné pour le sauvegarder ou l’envoyer vers le réseau
ou un périphérique. L’état d’un objet signifie l’ensemble des valeurs de ses champs.
L’opération inverse, qui consiste à reformer l’objet, s’appelle la désérialisation.
Les données peuvent être sérialisées sous de nombreuses formes : fichiers binaires,
XML, etc.

Les exceptions
Essayez de vous connecter avec un compte qui n’est pas membre du XNA Creators Club.
Que se passe-t-il ? La fenêtre du jeu se ferme et le focus est donné à Visual Studio où une
bien étrange boîte de dialogue est apparue. Cela signifie qu’une exception a été levée, en
l’occurrence GamerServicesNotAvailableException.

Figure 8-3
Une exception a été levée

Quand une erreur survient, une exception est levée. À partir de ce moment, l’exécution
normale est interrompue et un gestionnaire d’exceptions est recherché dans le bloc
d’instructions courant. S’il n’est pas trouvé, la recherche se poursuit dans le bloc englo-
=Labat FM.book Page 149 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
149

bant celui-ci ou, à défaut, dans le bloc de la fonction appelante, et ainsi de suite. Si la
recherche n’aboutit pas, une boîte de dialogue signalant l’exception s’affiche.
Si votre jeu avait été exécuté en mode Release, par exemple par un de vos amis dont vous
auriez aimé avoir l’avis, une boîte de dialogue peu commode se serait affichée (figure 8-4).

Figure 8-4
Voici la fenêtre qui apparaîtra si vous gérez mal vos exceptions

Vous allez donc devoir protéger votre jeu de ces arrêts brutaux en ajoutant autant de
gestionnaires d’exceptions que nécessaire. En pratique, il faut en ajouter chaque fois qu’une
portion de code est susceptible de rencontrer un problème : tentative de connexion au
réseau impossible, accès à des données qui n’existent pas, division par zéro, dépassement
de l’indice maximum lorsque vous manipulez des tableaux, etc. La liste peut être très
longue…
L’extrait de code ci-dessous vous présente la structure basique d’un gestionnaire
d’exceptions. Le code susceptible de générer une exception, et qui doit dont être testé, est
celui présent dans le bloc try. Vous pouvez ensuite récupérer l’exception pour la traiter
dans le bloc catch.
try
{
// Portion de code pouvant lancer une exception
}
catch(Exception e)
{
// Traitement de l'exception
}
L’exemple suivant divise une variable a par une variable b. Ici vous connaissez la valeur
de b, or cela n’est pas toujours le cas. Dans le doute, il est préférable d’ajouter un gestion-
naire d’exceptions pour se prémunir contre une division par zéro. Si une exception est
levée, sa description est affichée dans le titre de la fenêtre du jeu.
=Labat FM.book Page 150 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


150

int a = 10;
int b = 0;
try
{
a /= b;
}
catch (Exception e)
{
Window.Title = e.Message;
}

Dans l’intitulé de la fenêtre, vous pouvez lire :

Tentative de division par zéro.

En C#, les exceptions particulières dérivent toutes de la classe Exception. L’exemple


précédent a levé une exception de type DivideByZeroException, mais aucun bloc catch qui
lui correspond n’est présent. Il en existe tout de même un traitant une exception de type
Exception, c’est ce bloc qui sera exécuté.
Vous pouvez donc cumuler les blocs catch de manière à effectuer un traitement spécial
pour certaines erreurs. L’extrait de code ci-dessous reprend l’exemple précédent en affi-
chant un message spécial si l’exception levée est une division par zéro et un message plus
générique s’il s’agit d’une autre exception (sait-on jamais).
int a = 10;
int b = 0;
try
{
a /= b;
}
catch (DivideByZeroException e)
{
Window.Title = "Je le savais";
}
catch (Exception e)
{
Window.Title = e.Message;
}

Cette fois-ci, dans l’intitulé de la fenêtre, vous pouvez lire le message personnalisé.

Je le savais

Vous pouvez également ajouter un bloc finally qui sera exécuté, qu’une exception ait été
levée ou non.
int a = 10;
int b = 0;
=Labat FM.book Page 151 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
151

try
{
a /= b;
}
catch (DivideByZeroException e)
{
Window.Title = "Je le savais";
}
catch (Exception e)
{
Window.Title = e.Message;
}
finally
{
Window.Title = "Il est passé ici";
}
La levée d’une exception se fait en utilisant le mot-clé throw suivi d’un objet du type de
l’exception voulue. Dans l’exemple suivant, la fonction TestException lèvera une exception
si le paramètre i vaut 0.
protected override void Initialize()
{
base.Initialize();

try
{
TestException(0);
}
catch (Exception e)
{
Window.Title = e.Message;
}
}

private void TestException(int i)


{
if (i == 0)
throw new Exception("i vaut 0 !");
}

Personnaliser les exceptions


Vous pouvez créer vos propres exceptions selon vos besoins. Il suffit de les faire dériver de la classe
Exception.
=Labat FM.book Page 152 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


152

Les Gamer Services : interagir avec l’environnement


Les Gamer Services sont un ensemble de fonctionnalités qui permettent au jeu d’interagir
avec son environnement : boîte de dialogue pour informer l’utilisateur, récupérer des
messages de celui-ci, afficher sa liste d’amis et surtout, accéder à un périphérique de
sauvegarde.

Dossier de l’utilisateur
Commencez par ajouter au jeu un nouveau composant de type GamerServicesComponant,
faute de quoi vous ne pourrez pas utiliser le dossier de l’utilisateur.
this.Components.Add(new GamerServicesComponent(this));
Vous êtes à présent en mesure d’utiliser toutes les fonctionnalités mises à votre disposition
par la classe Guide (de l’espace de noms Microsoft.Xna.Framework.GamerServices). Par exem-
ple, le code ci-dessous affichera l’écran de connexion au Xbox LIVE (un abonnement
XNA Creators Club est requis pour pouvoir se connecter). Vous serez ensuite en mesure
de récupérer des informations à propos du joueur.

Figure 8-5
L’écran de connexion au Xbox LIVE s’affiche facilement

protected override void Initialize()


{
base.Initialize();
Guide.ShowSignIn(1, false);
}
=Labat FM.book Page 153 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
153

Grâce aux Gamer Services, vous accédez au dossier de l’utilisateur. Essayez la classe ci-
dessous. Si vous êtes sur Xbox 360, la méthode BeginShowStorageDeviceSelector() affichera
l’écran de sélection du périphérique de sauvegarde. Ensuite, la méthode EndShowStorage
DeviceSelector() renverra un objet StorageDevice. La méthode OpenContainer() de cet
objet renverra un objet de type StorageContainer que vous utiliserez pour vos fichiers de
sauvegarde.
Récupérer le dossier de l’utilisateur
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
StorageDevice device;
StorageContainer container;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null);
}
private void GetDevice(IAsyncResult result)
{
if (result.IsCompleted)
{
try
{
device = Guide.EndShowStorageDeviceSelector(result);
container = device.OpenContainer("ChapitreHuit");
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
}
private void EndShowMessageBox(IAsyncResult result)
{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
}
=Labat FM.book Page 154 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


154

Attention
La gestion du dossier de l’utilisateur dans un jeu sur la Xbox 360 peut s’avérer plus difficile que sous
Windows. En effet, il se peut par exemple qu’un joueur débranche le périphérique sélectionné en plein jeu.
Ne vous limitez donc pas sur l’utilisation de blocs try … catch.

Les méthodes asynchrones


Dans les exemples précédents, vous affichiez les détails des exceptions dans la barre de
titre des fenêtres. Ceci n’est pas très esthétique et reste peu pratique pour le joueur qui ne
remarquera pas forcément que le titre de la fenêtre à été modifié, surtout s’il joue en plein
écran… La solution est donc d’afficher le message dans la fenêtre du jeu : dans une
console ou bien dans une boîte de dialogue. Vous pourrez créer facilement une boîte de
dialogue si vous employez dans votre jeu un projet de GUI qui met ce genre d’éléments
à votre disposition, mais vous pouvez aussi simplement utiliser les Gamer Services.
La classe Guide possède une méthode BeginShowMessage (). Le tableau ci-dessous présente
les différents paramètres qu’elle attend.
Tableau 8-1 Paramètres de la méthode BeginShowMessage

Paramètre Description
PlayerIndex player Joueur concerné par la boîte de dialogue. Sous Windows, il ne peut
s’agir que du joueur 1.
String title Titre de la boîte de dialogue.

String text Contenu textuel de la boîte de dialogue.

IEnumerable<string> buttons Description à afficher sur chacun des boutons de la boîte de dialogue.
Il peut y avoir au maximum trois boutons.
Int focusButton Index (zéro étant la valeur minimale) du bouton qui doit avoir le focus.

MessageBoxIcon icon Type de l’icône à afficher avec la boîte de dialogue ( Alert, Error, None,
Warning).
AsyncCallback callback La méthode à appeler une fois que l’opération asynchrone est terminée.

Object state Un objet créé par l’utilisateur pour identifier l’appel à la méthode.

Vous vous demandez sûrement à quoi sert le paramètre callback de type AsyncCallback.
Généralement, l’appel à une fonction se fait de manière synchrone, c’est-à-dire qu’aucune
autre instruction n’est exécutée avant que la fonction ne retourne une valeur :
1. Appel de la fonction.
2. Exécution de la fonction.
3. Le thread qui a appelé la fonction récupère la main.
Or, le traitement de certaines fonctions peut être assez long, notamment dans le cas des
fonctions d’entrées-sorties ou de communication à travers le réseau. Il est donc nécessaire
de les traiter en parallèle, c’est-à-dire de manière asynchrone.
=Labat FM.book Page 155 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
155

Pour effectuer un traitement asynchrone, vous aurez donc besoin de trois méthodes. La
première, dont le nom commence par Begin, commande l’exécution de l’opération. Une
fois celle-ci terminée, un délégué (delegate en anglais, une variable permettant d’appeler une
fonction) est appelé. Celui-ci appelle alors la méthode dont le nom commence par End.
Commencez par écrire la fonction qui appellera la méthode dont le nom commence par
End. Elle doit prendre en paramètre une interface de type IAsyncResult, qui sera transmise
à la méthode commençant par End. Cette interface dispose du booléen IsCompleted
permettant de savoir si l’opération est terminée ou non.
private void EndShowMessageBox(IAsyncResult result)
{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
Il ne reste plus qu’à appeler la méthode commençant par Begin dans un bloc catch. N’oubliez
pas de passer en paramètre le délégué qui servira à appeler la fonction précédente.
Complétez le reste des paramètres selon vos besoins.
try
{
TestException(0);
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new String[]
➥ { "OK" }, 0, MessageBoxIcon.Error, new AsyncCallback(EndShowMessageBox), null);
}
Vous trouverez ci-dessous le code source complet de la classe illustrant la notion qui
vient d’être présentée.
Utiliser des méthodes asynchrones
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
try
{
TestException(0);
=Labat FM.book Page 156 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


156

}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ String[] { "OK" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
private void TestException(int i)
{
if (i == 0)
throw new Exception("i vaut 0 !");
}
private void EndShowMessageBox(IAsyncResult result)
{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.Black);
base.Draw(gameTime);
}
}

Figure 8-6
La boîte de dialogue affiche le détail de l’exception

Grâce aux Gamer Services, vous accédez également à une boîte de dialogue permettant à
l’utilisateur d’entrer une chaîne de caractères : il suffit d’utiliser les fonctions BeginShow
KeyboardInput () et EndShowKeyboardInput(). Le tableau ci-dessous répertorie tous les
paramètres attendus par la première fonction.
=Labat FM.book Page 157 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
157

Tableau 8-2 Paramètres de la fonction BeginShowKeyboardInput

Paramètre Description
PlayerIndex player Joueur concerné par la boîte de dialogue. Sous Windows, il ne peut
s’agit que du joueur un.
String title Titre de la boîte de dialogue.

String text Contenu textuel de la boîte de dialogue.

String defaultText Texte à afficher dans la boîte de dialogue lorsque celle-ci s’ouvre.

AsyncCallback callback La méthode à appeler une fois que l’opération asynchrone est terminée.

Object state Un objet créé par l’utilisateur pour identifier l’appel à la méthode.

Ci-dessous, vous retrouvez le code source d’une classe exemple utilisant ce type de boîte
de dialogue.

Utiliser la fenêtre KeyboardInput


public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.BeginShowKeyboardInput(PlayerIndex.One, "Entrez un message", "Entrez
➥ un message pour cet exemple", "", new AsyncCallback(EndShowKeyboardInput),
➥ null);
}
private void EndShowKeyboardInput(IAsyncResult result)
{
string userInput = Guide.EndShowKeyboardInput(result);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.Black);
base.Draw(gameTime);
}
}
=Labat FM.book Page 158 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


158

Figure 8-7
Le joueur peut aisément écrire un message dans cette boîte de dialogue

La GamerCard : la carte d’identité du joueur


Vous pouvez afficher les informations sur un joueur connecté grâce à la méthode
ShowGamerCard () de la classe Guide. Elle attend comme paramètre un PlayerIndex, ainsi
qu’un objet de type Gamer. Vous pouvez récupérer la liste des joueurs connectés grâce à la
collection SignedInGamers de la classe SignedInGamer.
La classe ci-dessous invite le joueur à se connecter puis, lorsque celui-ci pressera la
touche A de son clavier, elle affichera des informations le concernant. N’oubliez pas d’ajouter
un bloc try … catch, notamment au cas où aucun joueur ne serait connecté. Le test sur la
propriété IsVisible permet de ne pas demander un nouvel affichage du Guide si celui-ci
est déjà présent à l’écran.
Afficher la GamerCard du joueur connecté
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.ShowSignIn(1, true);
}
=Labat FM.book Page 159 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
159

protected override void Update(GameTime gameTime)


{
if (Keyboard.GetState().IsKeyDown(Keys.A) && !Guide.IsVisible)
{
try
{
Guide.ShowGamerCard(PlayerIndex.One, SignedInGamer
➥ .SignedInGamers[0]);
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}

base.Update(gameTime);
}
private void EndShowMessageBox(IAsyncResult result)
{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.Black);
base.Draw(gameTime);
}
}

Figure 8-8
À partir de ce panneau, le joueur peut administrer son compte
=Labat FM.book Page 160 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


160

De la même manière, vous pouvez permettre au joueur d’écrire un message avec Show
ComposeMessage(), d’afficher la liste de ses amis avec ShowFriends(), d’afficher la fenêtre
d’ajout d’un ami avec ShowFriendRequest(), etc. La liste est encore longue, à vous de
l’explorer et d’utiliser ces méthodes selon vos besoins.

Version démo
Si vous avez décidé de créer un jeu que vous vendrez ensuite à la communauté, vous
pouvez lui ajouter un mode de démonstration (trial mode). Bien évidemment, dans ce
mode, vous limiterez la liberté du joueur vis-à-vis des possibilités offertes par le jeu.
Pour que vous, développeur, puissiez tester ce mode démonstration, vous devez définir le
booléen SimulateTrialMode à true. Vous pouvez ensuite savoir si le jeu est exécuté en
mode démonstration via le booléen IsTrialMode.
Essayez la classe suivante.
Vérifier s’il s’agit d’une version d’essai
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

this.Components.Add(new GamerServicesComponent(this));
}

protected override void Update(GameTime gameTime)


{
if (Guide.IsTrialMode)
Window.Title = "Trial Mode";
else
Window.Title = "Full Mode";

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
graphics.GraphicsDevice.Clear(Color.Black);
base.Draw(gameTime);
}
}
=Labat FM.book Page 161 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
161

Dans la barre de titre de la fenêtre, vous pouvez lire :

Full Mode

Maintenant, modifiez la classe de manière à ce que la propriété SimulateTrialMode soit


définie à true.
Simuler une version d’essai
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.SimulateTrialMode = true;
}

protected override void Update(GameTime gameTime)


{
if (Guide.IsTrialMode)
Window.Title = "Trial Mode";
else
Window.Title = "Full Mode";

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
graphics.GraphicsDevice.Clear(Color.Black);
base.Draw(gameTime);
}
}
Vous pouvez à présent lire :

Trial Mode
=Labat FM.book Page 162 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


162

La sauvegarde en pratique : réalisation d’un éditeur de cartes


Pour vous aider à bien comprendre comment mettre en pratique une solution de sauve-
garde et de chargement de données dans le jeu, nous allons maintenant développer un
petit éditeur de cartes en 2D.

Identifier les besoins


Commençons par définir les besoins auxquels devra répondre l’éditeur de cartes.
Lorsque l’utilisateur appuiera sur la touche C de son clavier, une carte vierge sera générée.
L’utilisateur déplacera ensuite un curseur sur la carte grâce aux touches fléchées du clavier.
Il pourra ensuite choisir la texture à utiliser sur chacune des cases de la carte grâce aux
touches fonctions (F1, F2, etc.). Lorsque l’utilisateur pressera la touche S du clavier, la
carte sera sauvegardée. Il pourra ensuite fermer le programme, le rouvrir et recharger sa
carte en appuyant sur la touche L.
La première chose à faire est de créer un projet partagé de type Windows Game Library. Dans
ce projet, créez la classe correspondant aux cases de la carte. La classe s’appellera Tile.
Elle contiendra une texture, une chaîne de caractères correspondant au nom de fichier de la
texture et enfin, une paire de coordonnées correspondant à sa position logique sur la carte.
public class Tile
{
Texture2D texture;
string assetName;
public string AssetName
{
get
{
return assetName;
}
set
{
assetName = value;
}
}
Vector2 position;
public Vector2 Position
{
get
{
return position;
}
set
{
position = value;
}
}
=Labat FM.book Page 163 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
163

public Tile()
{
}

public Tile(string assetName, Vector2 position)


{
this.assetName = assetName;
this.position = position;
}

public void LoadContent(ContentManager Content)


{
texture = Content.Load<Texture2D>(assetName);
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, new Vector2(position.X * texture.Width, position.Y
➥ * texture.Height), Color.White);
}
}
Occupons-nous maintenant de la classe qui correspond à la carte. Cette classe contiendra
simplement un tableau de tableau de Tile.public class Map.
{
Tile[][] tiles;

public Tile[][] Tiles


{
get
{
return tiles;
}
set
{
tiles = value;
}
}

public Map()
{
}

public Map(Vector2 size)


{
tiles = new Tile[(int)size.Y][];
for(int i = 0; i < tiles.Length; i ++)
tiles[i] = new Tile[(int)size.X];
}
=Labat FM.book Page 164 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


164

public void LoadContent(ContentManager Content)


{
for (int y = 0; y < tiles.Length; y++)
{
for (int x = 0; x < tiles[0].Length; x++)
{
tiles[y][x].LoadContent(Content);
}
}
}
public void Draw(SpriteBatch spriteBatch)
{
for (int y = 0; y < tiles.Length; y++)
{
for (int x = 0; x < tiles[0].Length; x++)
{
tiles[y][x].LoadContent(Content);
}
}
}
}
La dernière classe à préparer est celle du curseur. Il s’agit d’un simple sprite qui devra se
déplacer selon les entrées clavier de l’utilisateur.
public class Cursor
{
Texture2D texture;
Vector2 position;
public Vector2 Position
{
get
{
return position;
}
set
{
position = value;
}
}
KeyboardState keyboardState;
KeyboardState lastKeyboardState;
public Cursor()
{
position = new Vector2(0, 0);
}
=Labat FM.book Page 165 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
165

public void LoadContent(ContentManager Content)


{
texture = Content.Load<Texture2D>("cursor");
}

public void Update(GameTime gameTime, Vector2 mapSize)


{
lastKeyboardState = keyboardState;

keyboardState = Keyboard.GetState();

if (keyboardState.IsKeyDown(Keys.Left) && lastKeyboardState


➥ .IsKeyUp(Keys.Left) && position.X > 0)
{
position.X--;
}
if (keyboardState.IsKeyDown(Keys.Right) && lastKeyboardState
➥ .IsKeyUp(Keys.Right) && position.X < mapSize.X-1)
{
position.X++;
}
if (keyboardState.IsKeyDown(Keys.Up) && lastKeyboardState.IsKeyUp(Keys.Up)
➥ && position.Y > 0)
{
position.Y--;
}
if (keyboardState.IsKeyDown(Keys.Down) && lastKeyboardState
➥ .IsKeyUp(Keys.Down) && position.Y < mapSize.Y-1)
{
position.Y++;
}
}

public void Draw(SpriteBatch spriteBatch)


{
spriteBatch.Draw(texture, new Vector2(position.X * texture.Width, position.Y
➥ * texture.Height), Color.White);
}
}
Pour terminer, référencez le projet de type Windows Game Library dans le projet principal.
Pour ce faire, dans l’explorateur de solution faites un clic droit sur le conteneur de Réfé-
rences du projet principal, puis choisissez Ajouter une référence. Allez ensuite sur l’onglet
Projet et choisissez le projet partagé.
Nous avons maintenant tous les outils en main pour mettre en place l’éditeur de cartes.
Ajoutons des objets de type Map et Cursor à la classe principale du projet. Si l’utilisateur
appuie sur la touche C du clavier, nous initialisons la carte et le curseur. Dans le cas de la
carte, on charge la même texture pour toutes les cases de la carte.
=Labat FM.book Page 166 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


166

Ensuite, si la carte et le curseur existent bien, nous vérifierons si une touche Fx est pressée
et modifions la texture de la case concernée en conséquence.
public class Chapitre_8 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

KeyboardState keyboardState;
KeyboardState lastKeyboardState;

Map map;
Cursor cursor;

Vector2 mapSize = new Vector2(5, 5);

public Chapitre_8()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

graphics.PreferredBackBufferHeight = 160;
graphics.PreferredBackBufferWidth = 160;
}

protected override void Initialize()


{
base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
lastKeyboardState = keyboardState;
keyboardState = Keyboard.GetState();

if(keyboardState.IsKeyDown(Keys.C) && lastKeyboardState.IsKeyUp(Keys.C))


{
map = new Map();

map = new Map(mapSize);


=Labat FM.book Page 167 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
167

for (int y = 0; y < map.Tiles.Length; y++)


{
for (int x = 0; x < map.Tiles[0].Length; x++)
{
map.Tiles[y][x] = new Tile("grass", new Vector2(x, y));
}
}
map.LoadContent(Content);
cursor = new Cursor();
cursor.LoadContent(Content);
}
else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState
➥ .IsKeyUp(Keys.S) && map != null)
{
}
else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState
➥ .IsKeyUp(Keys.L))
{
}
if (cursor != null && map != null)
{
cursor.Update(gameTime, mapSize);
if (keyboardState.IsKeyDown(Keys.F1) && lastKeyboardState
➥ .IsKeyUp(Keys.F1))
{
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName
➥ = "grass";
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X]
➥ .LoadContent(Content);
}
else if (keyboardState.IsKeyDown(Keys.F2) && lastKeyboardState
➥ .IsKeyUp(Keys.F2))
{
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName
➥ = "tree";
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X]
➥ .LoadContent(Content);
}
else if (keyboardState.IsKeyDown(Keys.F3) && lastKeyboardState
➥ .IsKeyUp(Keys.F3))
{
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName
➥ = "sand";
map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X]
➥ .LoadContent(Content);
}
}
}
=Labat FM.book Page 168 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


168

protected override void Draw(GameTime gameTime)


{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
if (map != null)
map.Draw(spriteBatch);
if (cursor != null)
cursor.Draw(spriteBatch);
spriteBatch.End();
}
Vous pouvez à présent exécuter l’éditeur de cartes et vous amuser un peu avec (figure 8-9).
Figure 8-9
L’éditeur de cartes

Chemin du dossier de jeu


Vous pouvez récupérer le chemin du dossier du jeu grâce à la propriété statique Title
Location de l’objet StorageContainer. L’exemple suivant affiche ce chemin à la place du
titre du jeu.
Récupérer le chemin du dossier de jeu
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
Window.Title = StorageContainer.TitleLocation;
base.Initialize();
}
=Labat FM.book Page 169 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
169

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
}

protected override void UnloadContent()


{
}

protected override void Update(GameTime gameTime)


{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

base.Draw(gameTime);
}
}

Figure 8-10
Le chemin complet vers le dossier du jeu

Gérer les dossiers


C’est l’espace de noms System.IO qui contient les classes permettant de gérer fichiers et
répertoires. Le tableau ci-dessous présente quelques-unes des méthodes statiques mises à
disposition par la classe Directory. Il en existe beaucoup d’autres, mais seules celles-ci
seront utilisées dans le cas présent.

Tableau 8-3 Méthodes de la classe Directory

Méthode Description
Bool Exists(string path) Renvoie vrai si le répertoire correspondant au chemin passé
en argument existe.
DirectoryInfo CreateDirectory(string path) Crée tous les répertoires et sous-répertoires correspondant
au chemin passé en argument.
Void Delete(string path) Supprime le répertoire correspondant au chemin passé en
argument.
=Labat FM.book Page 170 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


170

L’exemple suivant utilise ces trois méthodes. Si le joueur presse la touche C et que le
répertoire test n’existe pas dans le dossier de l’utilisateur (on récupère le chemin via la
propriété Path de l’objet de type StorageContainer), on le crée. Si le joueur presse la
touche D et que le répertoire existe, on le supprime.
Gérer les dossiers dans le répertoire de l’utilisateur
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

StorageDevice device;
StorageContainer container;

public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();

Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null);


}

protected override void Update(GameTime gameTime)


{
if (Keyboard.GetState().IsKeyDown(Keys.C))
{
try
{
if(!Directory.Exists(Path.Combine(container.Path, "test")))
Directory.CreateDirectory(Path.Combine(container.Path, "test"));
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}

else if (Keyboard.GetState().IsKeyDown(Keys.D))
{
try
{
=Labat FM.book Page 171 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
171

if (Directory.Exists(Path.Combine(container.Path, "test")))
Directory.Delete(Path.Combine(container.Path, "test"));
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

base.Draw(gameTime);
}

private void GetDevice(IAsyncResult result)


{
if (result.IsCompleted)
{
try
{
device = Guide.EndShowStorageDeviceSelector(result);
container = device.OpenContainer("ChapitreHuit");
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
}

private void EndShowMessageBox(IAsyncResult result)


{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
}
Dans cet exemple, vous remarquez que la méthode Combine de la classe Path est utilisée
pour créer le chemin du répertoire. Vous n’avez donc pas à vous soucier des séparateurs /
ou \.
=Labat FM.book Page 172 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


172

La classe Directory possède d’autres méthodes qui pourront vous servir dans le dévelop-
pement des jeux. Ainsi, la méthode GetFiles() retournera un tableau de chaînes de
caractères correspondant à la liste des fichiers présents dans un répertoire. L’une de ses
surcharges vous permet également d’ajouter un filtre sur les noms de fichiers à récupérer.

Manipuler les fichiers


La manipulation de fichiers se fera grâce à la classe File. Elle possède un très grand
nombre de méthodes, mais nous ne les présenterons pas toutes ici. Pour commencer,
cette classe dispose, comme la classe Directory, d’une méthode Exists() fonctionnant
exactement de la même manière.
Pour créer un fichier, vous pouvez utiliser la méthode Create (). Elle attend comme seul
paramètre le chemin du fichier à créer. Cependant, elle dispose de surcharges vous
permettant de définir la taille du buffer qui sera utilisé pour la lecture et l’écriture, la
méthode de création du fichier ou encore les options de sécurité à appliquer au fichier. La
méthode retourne un objet de type FileStream. Si vous ne souhaitez pas modifier tout de
suite le contenu du fichier, fermez-le directement avec la fonction Close().

Bonne pratique
Une bonne habitude à prendre en programmation est de toujours fermer les fichiers que vous avez créés
ou ouverts lorsque vous n’en avez plus besoin.

Vous pouvez copier un fichier grâce à la méthode Copy (). Elle prend en paramètre le
chemin du fichier à copier et le chemin du fichier de destination. Elle possède une
surcharge qui vous propose de définir si le fichier de destination doit être écrasé lorsqu’il
existe déjà.
Pour supprimer un fichier, utilisez simplement la méthode Delete () qui attend comme
argument le chemin du fichier à supprimer.
La classe ci-dessous utilise toutes ces méthodes. Si le joueur appuie sur la touche C et
que le fichier test.sav n’existe pas dans le dossier de l’utilisateur, il est créé ; s’il existe,
il est copié en test_copy.sav en écrasant le fichier de destination. Si le joueur appuie
sur D et que test.sav existe, il est supprimé.
Gérer les fichiers dans le répertoire de l’utilisateur
public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

StorageDevice device;
StorageContainer container;
=Labat FM.book Page 173 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
173

public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null);
}
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.C))
{
try
{
if(!File.Exists(Path.Combine(container.Path, "test.sav")))
File.Create(Path.Combine(container.Path, "test.sav")).Close();
if(File.Exists(Path.Combine(container.Path, "test.sav")))
File.Copy(Path.Combine(container.Path, "test.sav")
➥ ,Path.Combine(container.Path, "test_copy.sav"), true);
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
else if (Keyboard.GetState().IsKeyDown(Keys.D))
{
try
{
if (File.Exists(Path.Combine(container.Path, "test.sav")))
File.Delete(Path.Combine(container.Path, "test.sav"));
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
base.Update(gameTime);
}
=Labat FM.book Page 174 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


174

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

base.Draw(gameTime);
}

private void GetDevice(IAsyncResult result)


{
if (result.IsCompleted)
{
try
{
device = Guide.EndShowStorageDeviceSelector(result);
container = device.OpenContainer("ChapitreHuit");
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
}

private void EndShowMessageBox(IAsyncResult result)


{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
}

Écrire dans un fichier


Pour écrire dans un fichier, commencez par récupérer un flux en écriture vers le fichier
désiré. Cette opération se fera grâce à la méthode Open () de la classe File. Le tableau 8-4
détaille les paramètres attendus par la méthode.

Tableau 8-4 Paramètres de la méthode Open

Paramètre Description
String path Chemin du fichier à ouvrir.

FileMode mode Méthode d’ouverture à utiliser sur le fichier. FileMode est une énumération qui peut
prendre plusieurs valeurs (ajout à la fin du fichier, vider le fichier, ouverture classique,
etc.).
FileAccess access Définit les opérations qui pourront être effectuées sur le fichier (lecture, écriture ou les
deux).
FileShare share Définit le type d’accès que les autres threads ont sur le fichier.
=Labat FM.book Page 175 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
175

Utilisez ensuite un objet de type StreamWriter pour écrire dans le flux récupéré précédem-
ment. Comme pour le flux de sortie standard (souvenez-vous de vos premiers programmes
en mode console), l’écriture se fait grâce aux méthodes Write () et WriteLine (). N’oubliez
pas ensuite de vider le buffer grâce à la méthode Flush () et enfin de fermer le flux par la
méthode Close ().
La classe suivante met ces actions en pratique. Lorsque le joueur appuie sur la touche O,
on vérifie si le fichier test.sav existe dans le répertoire de l’utilisateur. Si c’est le cas, on
se place à la fin du fichier, sinon il est créé. Il ne reste plus qu’à ajouter une ligne au flux,
puis à fermer le fichier proprement. Après avoir testé le code, vous lirez dans le fichier la
phrase suivante :

XNA’s Not Acronymed

Ouvrir un fichier et écrire dans un fichier


public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
StorageDevice device;
StorageContainer container;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null);
}
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.O))
{
try
{
FileStream file = File.Open(Path.Combine(container.Path,
➥ "test.sav"), FileMode.Append);
StreamWriter fileWriter = new StreamWriter(file);
fileWriter.WriteLine("XNA's Not Acronymed");
fileWriter.Flush();
fileWriter.Close();
}
=Labat FM.book Page 176 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


176

catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

base.Draw(gameTime);
}

private void GetDevice(IAsyncResult result)


{
if (result.IsCompleted)
{
try
{
device = Guide.EndShowStorageDeviceSelector(result);
container = device.OpenContainer("ChapitreHuit");
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
}

private void EndShowMessageBox(IAsyncResult result)


{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
}

Lire un fichier
C’est une bonne chose de savoir écrire, c’est encore mieux de pouvoir lire ce qui a été
écrit. Ainsi, pour lire des données écrites en clair, commencez par récupérer un flux
FileStream que vous exploiterez grâce aux méthodes Read(), ReadLine(), ReadToEnd(), etc.,
d’un objet StreamReader.
=Labat FM.book Page 177 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
177

Lire le contenu d’un fichier


public class ChapitreHuit : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
StorageDevice device;
StorageContainer container;
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}

protected override void Initialize()


{
base.Initialize();
Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null);
}
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.L) && File.Exists
➥ (Path.Combine(container.Path, "test.sav")))
{
try
{
FileStream file = File.Open(Path.Combine(container.Path,
➥ "test.sav"), FileMode.Open);
StreamReader fileReader = new StreamReader(file);
Window.Title = fileReader.r();
fileReader.Close();
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);

base.Draw(gameTime);
}
=Labat FM.book Page 178 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


178

private void GetDevice(IAsyncResult result)


{
if (result.IsCompleted)
{
try
{
device = Guide.EndShowStorageDeviceSelector(result);
container = device.OpenContainer("ChapitreHuit");
}
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message,
➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
}
private void EndShowMessageBox(IAsyncResult result)
{
if (result.IsCompleted)
Guide.EndShowMessageBox(result);
}
}

Sérialiser des données


Reprenons maintenant l’éditeur de cartes que nous avons commencé à réaliser plus tôt
dans ce chapitre. Nous allons à présent voir comment sérialiser les données : dans un
premier temps en binaire, puis en XML.
Pour qu’une classe puisse être sérialisée, il faut lui ajouter l’attribut [Serializable].
Ajoutons-le aux classes Map et Tile.
[Serializable]
public class Map
{ … }

[Serializable]
public class Tile
{ … }
Nous ne devons pas sérialiser la texture de chaque Tile, mais seulement son nom. Pour
qu’un attribut ne soit pas sérialisé, il faut le marquer comme [NonSerialized].
[NonSerialized]
Texture2D texture;
Pour sérialiser les données en binaire, utilisez l’espace de noms System.Runtime.Serialization
.Formatters.Binary. Ensuite, si le joueur presse la touche S, vérifiez l’existence du fichier
de destination et créez-le si nécessaire, sinon ouvrez-le (dans l’exemple, il est tronqué à
l’ouverture). Utilisez ensuite un objet de type BinaryFormatter et sa méthode Serialize()
à laquelle vous devrez passer le flux où écrire et l’objet à sérialiser. Pour terminer, fermez
le flux.
=Labat FM.book Page 179 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
179

else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState.IsKeyUp(Keys.S) && map


➥ != null)
{
FileStream file;

if (!File.Exists(Path.Combine(container.Path, "test.sav")))
file = File.Create(Path.Combine(container.Path, "test.sav"));
else
file = File.Open(Path.Combine(container.Path, "test.sav"),
➥ FileMode.Truncate);

BinaryFormatter serializer = new BinaryFormatter();


serializer.Serialize(file, map);
file.Close();
}
Si vous vous rendez dans le répertoire de sauvegarde du jeu, vous verrez le fichier
test.sav. Vous pourrez ensuite l’ouvrir et constater que les données ne sont pas du tout
intelligibles (figure-8-11).

Figure 8-11
Les deux fichiers créés par la classe exemple

Vous pouvez également sérialiser les données sous forme de fichier XML. L’espace de
noms à utiliser est System.XML.Serialization. Une classe peut être sérialisée en XML
uniquement si elle possède un constructeur sans paramètres. Il faut également que tous
les attributs à sérialiser possèdent un accesseur en lecture et un autre en écriture. S’il y a
des paramètres que la sérialisation doit ignorer, utilisez l’attribut [XmlIgnore].
Du côté de la classe Chapitre_8, instanciez un objet XmlSerializer auquel vous devrez
passer le type d’objet à sérialiser. Servez-vous pour cela de l’opérateur typeof(). Enfin,
comme pour le BinaryFormatter, utilisez la méthode Serialize() qui attend les mêmes
arguments. N’oubliez pas de fermer le flux après utilisation.
=Labat FM.book Page 180 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


180

else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState.IsKeyUp(Keys.S) && map


➥ != null)
{
FileStream file;

if (!File.Exists(Path.Combine(container.Path, "test.xml")))
file = File.Create(Path.Combine(container.Path, "test.xml"));
else
file = File.Open(Path.Combine(container.Path, "test.xml"),
➥ FileMode.Truncate);

XmlSerializer serializer = new XmlSerializer(typeof(Map));


serializer.Serialize(file, map);
file.Close();
}
Vous pouvez tester l’éditeur en créant une carte et en la sauvegardant : un fichier XML
est bel et bien généré (figure-8-12).

Figure 8-12
Le fichier XML représentant
la carte

Désérialiser des données


Vous savez maintenant comment sauvegarder des données, il ne vous reste plus qu’à
apprendre à les charger ! Ce processus se fera très simplement en utilisant la méthode
Deserialize () de l’objet BinaryFormatter. Il faut ensuite effectuer un cast (c’est-à-dire
une modification de type) de l’objet retourné par la fonction pour récupérer un objet Map.
=Labat FM.book Page 181 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
181

Après avoir récupéré l’objet Map, n’oubliez pas d’appeler la méthode LoadContent() pour
charger les textures.
else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState.IsKeyUp(Keys.L))
{
FileStream file = File.Open(Path.Combine(container.Path, "test.sav"),
➥ FileMode.Open);
BinaryFormatter serializer = new BinaryFormatter();
map = (Map)serializer.Deserialize(file);
file.Close();

map.LoadContent(Content);

cursor = new Cursor();


cursor.LoadContent(Content);
}
Voyons à présent la désérialisation XML. Dans la classe Chapitre_8, vous utiliserez la
méthode Deserialize() à laquelle vous passerez le flux à traiter. Transformez ensuite
l’objet retourné par la méthode en un objet de type Map avec la méthode du casting.
Attention, lorsque vous utilisez la sérialisation XML : tout n’est pas sérialisable. C’est
pour cette raison que nous avons utilisé un tableau de tableau de Tile plutôt qu’un tableau
à deux dimensions.
else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState.IsKeyUp(Keys.L))
{
FileStream file = File.Open(Path.Combine(container.Path, "test.xml"),
➥ FileMode.OpenOrCreate);
XmlSerializer deserializer = new XmlSerializer(typeof(Map));
map = (Map)deserializer.Deserialize(file);
file.Close();

map.LoadContent(Content);

cursor = new Cursor();


cursor.LoadContent(Content);
}
L’éditeur de cartes est maintenant prêt : vous pouvez créer, sauvegarder et charger des
cartes en utilisant soit la sérialisation binaire, soit la sérialisation XML.

Les Content Importers, une solution compatible avec


la Xbox 360
Si vous essayez de désérialiser une carte comme vous venez de le voir dans un projet
exécuté sur Xbox 360, vous vous rendrez compte que cela ne fonctionne pas. Sur cette
plate-forme, vous ne pouvez charger que des fichiers .xnb et, pour générer de tels fichiers
à partir des types personnalisés (comme la classe Map), vous devrez créer un ContentImporter.
=Labat FM.book Page 182 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


182

1. Ajoutez un projet de type ContentPipelineExtension à la solution.


2. Ajoutez à ce nouveau projet une référence vers le projet partagé qui contient les
classes Map, Tile et Cursor.
3. Ajoutez ensuite au projet un nouvel élément de type Content Type Writer que vous
nommerez TileWriter.
4. Renseignez le type de données concernées par le Content Type Writer.
using TWrite = Chapitre_8_Shared.Tile;
5. Il faut ensuite compléter la méthode Write(). Le principe est simple : vous disposez
de l’objet (value) et vous écrivez le contenu de ses différents paramètres sur l’objet
output.
6. Écrivez le nom de sa texture en utilisant la méthode Write() de l’objet output. De la
même manière, occupez-vous de l’attribut position.
protected override void Write(ContentWriter output, TWrite value)
{
output.Write(value.AssetName);

output.Write(value.Position);
}
À présent, les données de type Tile peuvent être sérialisées vers un fichier .xnb.
Intéressons-nous maintenant à leur lecture.
1. Dans le projet de bibliothèque de classes, ajoutez un nouvel élément de type Content
Type Reader.
2. Comme pour le Writer, commencez par renseigner le type de données concernées.
Using Tread = Chapitre_8_Shared.Tile;
3. À présent, c’est la méthode Read() que vous allez devoir compléter. Commencez par
créer une nouvelle instance de Tile. Ensuite, récupérez la valeur des deux attributs
grâce aux méthodes ReadString() et ReadVector2(). Enfin, n’oubliez pas de retourner
l’objet recréé.
protected override TRead Read(ContentReader input, TRead existingInstance)
{
existingInstance = new TRead();

existingInstance.AssetName = input.ReadString();

existingInstance.Position = input.ReadVector2();

return existingInstance;
}
4. Il reste une dernière chose à faire pour la classe Tile. Retournez sur le Writer et
complétez la méthode GetRuntimeReader().
=Labat FM.book Page 183 Vendredi, 19. juin 2009 4:01 16

Exceptions et gestion des fichiers : sauvegarder et charger un niveau


CHAPITRE 8
183

public override string GetRuntimeReader(TargetPlatform targetPlatform)


{
return typeof(Chapitre_8_Shared.TileReader).AssemblyQualifiedName;
}
C’est fini pour la classe Tile.
5. Répétez ces opérations pour la classe Map. Il y a cependant un petite différence : vous
ne pouvez pas utiliser la méthode Write() puisque vous avez défini le type de l’attribut
Tiles (un tableau de tableau de Tile). Vous devrez utiliser la méthode générique
WriteObject().
protected override void Write(ContentWriter output, TWrite value)
{
output.WriteObject<Chapitre_8_Shared.Tile[][]>(value.Tiles);
}
6. Le raisonnement est le même que pour le Reader : vous devez utiliser la méthode
ReadObject().
protected override TRead Read(ContentReader input, TRead existingInstance)
{
existingInstance = new Map();

existingInstance.Tiles = input.ReadObject<Tile[][]>();

return existingInstance;
}
7. N’oubliez pas la méthode GetRuntimeReader().
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
return typeof(Chapitre_8_Shared.MapReader).AssemblyQualifiedName;
}
8. Le ContentImporter est fin prêt. Cependant, vous devez lui fournir un fichier XML en
entrée. Pour créer ce fichier, commencez par ajouter une nouvelle référence au projet
principal vers l’assembly Microsoft.XNA.Framework.Content.Pipeline. Ajoutez ensuite
une directive using à la classe Chapitre_8.
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;
9. Pour générer un fichier XML correspondant à la carte, vous utiliserez un objet de
type XmlWritter qui correspondra au flux de sortie. Vous sérialisez ensuite les données
grâce à la méthode statique Serialize de la classe IntermediateSerializer().
XmlWriterSettings xmlSettings = new XmlWriterSettings();
xmlSettings.Indent = true;

using (XmlWriter xmlWritter = XmlWriter.Create("map.xml", xmlSettings))


{
IntermediateSerializer.Serialize(xmlWritter, map, null);
}
=Labat FM.book Page 184 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


184

Vous pouvez exécuter l’éditeur, créer une carte et la sauvegarder.


10. Rendez-vous dans le répertoire de sortie du projet et vérifiez l’existence du fichier
map.xml.
11. À présent, ajoutez une référence vers le projet d’extension du Content Pipeline au
projet de contenu. Ajoutez ensuite le fichier map.xml au projet de contenu comme
vous ajouteriez n’importe quel autre type de ressource.
À ce stade, si vous compilez le projet, vous remarquez que la génération d’un fichier .xnb
correspondant à la carte. Il ne vous reste plus qu’à le charger comme vous le feriez avec
n’importe quel type de ressource de base de XNA.
map = Content.Load<Map>("map");

En résumé
Dans ce chapitre vous avez découvert :
• les différents types d’espace de stockage disponibles avec XNA ;
• ce qu’est un fichier XML et comment en créer un ;
• comment manipuler dossiers et fichiers en C# ;
• ce qu’est la sérialisation, qu’elle soit binaire ou XML, et comment l’utiliser dans le
cadre d’un projet sous XNA.
=Labat FM.book Page 185 Vendredi, 19. juin 2009 4:01 16

9
Pathfinding : programmer
les déplacements
des personnages

Comment le héros d’un jeu vidéo fait-il pour trouver rapidement la sortie d’un labyrinthe
alors que le joueur a simplement cliqué sur la sortie de celui-ci ? Après une introduction
sur l’intelligence artificielle et les algorithmes de recherche de chemin, vous découvrirez
en détail l’algorithme A*, puis vous apprendrez à le mettre en œuvre. À la fin de chapitre
vous serez donc capable de répondre à cette question, et même mieux, vous apprendrez à
programmer le comportement du héros qui doit sortir du labyrinthe.

Les enjeux de l’intelligence artificielle


Pour rendre un jeu intéressant pour le joueur, vous allez par exemple devoir y introduire
des mécanismes liés à l’intelligence artificielle.
Le terme intelligence artificielle est difficilement définissable, puisque même les experts
du domaine ne s’accordent pas sur sa signification. Certains diront qu’une machine est
intelligente dès lors qu’elle sait reproduire le comportement d’un être humain, par exemple
en accomplissant des tâches que l’homme sait lui aussi accomplir grâce à son intelligence.
À cela, d’autres répondront qu’on ne peut pas parler ici d’intelligence, mais de simple
copie du comportement. Il y a de très nombreuses choses à dire sur ce débat, puisque
celui-ci relève même pour certaines personnes du domaine de l’éthique. Mais là n’est pas
l’objectif de ce chapitre !
=Labat FM.book Page 186 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


186

La recherche sur l’intelligence artificielle évolue rapidement. De nos jours, un champion


du monde d’échecs se fait battre par un ordinateur. Peut-être que dans un avenir proche,
les choses vont prendre une dimension encore plus grande. Des scientifiques travaillent
sur un système capable de représenter des pensées en images, d’autres ont créé un robot
qui fonctionne avec un cerveau contrôlé par des neurones de rat. Le futur présenté dans
les films de science-fiction de ces 30 dernières années semble arriver bien plus vite que
n’importe qui aurait pu l’imaginer.
XNA ne propose pas de classes ou de fonctions prêtes à l’emploi en rapport avec n’importe
quel domaine de l’intelligence artificielle. C’est à vous, développeur, de programmer
pour les jeux les algorithmes qui vous intéressent.

Comprendre le pathfinding
En programmation de jeu vidéo, on appelle pathfinding (recherche de chemin, en français)
le processus de détermination du chemin entre un point de départ et un point d’arrivée.
Le tableau 9-1 présente quelques algorithmes de recherche de chemin.

Tableau 9-1 Algorithmes de recherche de chemin

Nom Description
Dijkstra Il retourne le meilleur chemin. Il est utilisé par exemple dans certains protocoles de
routage réseau.
Viterbi Il permet de corriger les erreurs survenues lors d’une transmission via un canal bruité.
A* (prononcez « A Star ») Il ne retourne pas forcément la meilleure solution, mais c’est un bon compromis entre
pertinence du résultat et coût du calcul.

Les domaines d’application du pathfinding sont nombreux et variés : GPS, robotique, réseaux
informatiques, jeux vidéo etc. Dans tous ces domaines, le processus de détermination du
chemin à emprunter est essentiel.
Ainsi, si on considère les jeux de stratégie, des milliers de personnages peuvent se déplacer
en même temps : l’ordinateur ou la console effectue sans relâche des calculs pour
permettre aux différentes entités de se déplacer. Il faut donc trouver un moyen rapide
d’effectuer ces calculs tout en conservant des résultats pertinents. Soyez cependant vigilant :
si la solution que vous mettez en place ne retourne pas des résultats assez rapidement, le
jeu sera saccadé. En vous reportant au tableau précédent, vous trouverez facilement quel
algorithme est adapté aux jeux vidéo.
Effectivement, il s’agit de A*. Dans la suite de ce chapitre, nous nous intéresserons à son
principe de fonctionnement, puis à sa mise en œuvre en C# avec XNA. Attention, il a été
question de performances quelques lignes plus tôt : l’algorithme tel qu’il est présenté ici
est loin d’être vraiment utilisable dans une grosse production. Il y a beaucoup de choses
à améliorer pour réduire le temps nécessaire à son exécution. Cependant, ce chapitre
n’est qu’une introduction à la recherche de chemin. Si vous vous intéressez aux bonnes
=Labat FM.book Page 187 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
187

pratiques en matière d’optimisation d’algorithmes, l’Internet fourmille d’articles à ce


sujet, consultez-les.

L’algorithme A* : compromis entre performance et pertinence


Dans cette section, nous allons nous pencher sur le fonctionnement de l’algorithme et
verrons un exemple d’utilisation. Vous serez ainsi capable de l’utiliser dans vos jeux.

Principe de l’algorithme
Nous allons à présent travailler sur une carte représentant le niveau d’un jeu. Chaque case
qui constitue la carte est appelée nœud. Le point de départ sera symbolisé par une case
verte et le point d’arrivée, par une case rouge. Les cases blanches symbolisent des cases
classiques, franchissables sans effort particulier. Les cases bleues symbolisent l’eau et sont
plus difficiles à franchir que les cases classiques. Enfin, les cases grises symbolisent des
murs parfaitement infranchissables. La figure 9-1 donne un exemple de ce type de carte.
Figure 9-1
Le type de carte qui sera
utilisé

Le travail de recherche de chemin s’effectue grâce à deux listes de nœuds :


• une liste ouverte, qui contient les nœuds susceptibles de conduire le joueur au nœud de
destination, c’est-à-dire les nœuds à vérifier ;
• et une liste fermée, contenant les nœuds déjà traités qui composeront le chemin final.
Intéressons-nous d’abord à la première phase de l’algorithme.
Commencez par ajouter le point de départ à la liste fermée, puisque la solution passera
forcément par lui. Intéressez-vous ensuite à tous les points voisins de ce point de départ
et ajoutez-les également à la liste ouverte, tout en ignorant ceux qui sont infranchissables
(les murs dans notre exemple).
=Labat FM.book Page 188 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


188

Déplacement oblique
Ici, les déplacements se font uniquement à la verticale et à l’horizontale. Cependant, vous pouvez bien sûr
utiliser l’algorithme avec des déplacements obliques.

À présent, il nous faut définir leur parent, c’est-à-dire le nœud qui permet d’y arriver.
Pour l’instant, dans notre cas, il s’agit du nœud de départ. Ensuite, choisissez le nœud
que vous devrez inspecter après le nœud de départ. Pour cela, déterminez lequel de ces
nœuds est le plus proche du nœud de destination.
Pour déterminer cette distance, il faut calculer ce que l’on appelle la distance de Manhattan.
Cette distance correspond au nombre de déplacement horizontaux et verticaux qui devront
être effectués pour aller d’un point à un autre. Si nous reprenons l’exemple des figures
précédentes, cela donne dans les deux cas 16 déplacements à effectuer. Pour le nœud au-
dessus du nœud de départ, il y a 15 déplacements horizontaux et un vertical. Pour le nœud
à droite du nœud de départ, il y a 14 déplacements horizontaux et 2 verticaux.
Nous pouvons donc utiliser n’importe lequel des deux nœuds pour continuer notre recherche.
Choisissez-en un, retirez-le de la liste ouverte et ajoutez-le à la liste fermée. Puis répétez
les opérations d’analyse et de détermination du meilleur nœud voisin, jusqu’à arriver au
nœud de destination. Une fois à destination, remontez de nœud en nœud grâce au nœud
parent que vous avez défini pour chacun d’entre eux, jusqu’à arriver au nœud de départ
qui n’aura pas de parent.

Attention
Si, à un moment donné, la liste ouverte ne contient plus de nœud, mais que vous n’êtes pas encore arrivé
au nœud de destination, c’est qu’il n’existe pas de chemin possible vers ce nœud.

Figure 9-2
Analyse
du second nœud
et de ses voisins…
=Labat FM.book Page 189 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
189

Figure 9-3
… jusqu’au nœud
de destination

Dans le calcul de l’estimation de la distance vers le nœud de destination, il est possible


d’ajouter la notion de coût du passage sur un nœud. Le chemin déterminé, visible sur les
figures précédentes, utilise cette notion. Le passage sur une case classique n’a pas de coût
particulier, alors que le passage sur une case eau (bleue) a un coût de deux. Si vous
supprimez la zone d’eau ou que vous diminuez son coût, l’algorithme préférera passer
par cette zone (figure 9-4). On peut également imaginer la présence d’un pont au-dessus
du cours d’eau : ainsi, en ajustant bien les coûts, l’algorithme préférera emprunter le pont
plutôt que d’envoyer le personnage dans l’eau.

Figure 9-4
Sans la zone d’eau, l’algo-
rithme détermine un autre
chemin
=Labat FM.book Page 190 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


190

Implanter l’algorithme dans un jeu de type STR


Maintenant que vous connaissez les bases du fonctionnement de l’algorithme, cette
section explique comment l’appliquer à un début de jeu de stratégie en temps réel (STR).

La classe Tile et la carte


Créez un nouveau projet baptisé « ChapitreNeuf » et renommez la classe Game1 en
ChapitreNeuf. Ajoutez la classe Sprite telle qu’elle était à la fin du chapitre 6, puis créez
une classe dérivée de Sprite que vous appellerez Tile.
Une carte est composée à l’écran de plusieurs objets de type Tile, il s’agit donc de « cases ».
Il en existe trois types :
• une case normale ;
• une case représentant de l’eau ;
• une case représentant un mur.
Créez une énumération chargée de représenter ces trois cas.
enum TileType
{
Wall = -1,
Normal = 0,
Water = 2,
};
Il est également nécessaire d’ajouter une nouvelle paire de coordonnées (x, y) à cette
classe. En effet, les coordonnées fournies par la classe Sprite représentent les coordonnées
à l’écran or, ici, il faudra travailler avec les coordonnées sur une carte de jeu. Ces nouvelles
coordonnées représenteront la position de l’objet Tile dans un tableau à deux dimensions,
la valeur y étant la position sur la première dimension et la valeur x, la position sur la
deuxième dimension.
Chaque case sera représentée à l’écran par un carré de 32 ¥ 32 pixels qui sera coloré selon
son type : gris si c’est un mur, bleu pour l’eau, vert pour le point de départ, rouge pour le point
d’arrivée et enfin, orange pour les points composant le chemin retourné par l’algorithme.
La classe Tile
class Tile : Sprite
{
int x;
public int X
{
get { return this.x; }
set { this.x = value ; }
}
int y;
=Labat FM.book Page 191 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
191

public int Y
{
get { return this.y; }
set { this.x = value ; }
}
TileType type;
public TileType Type
{
get { return type; }
}
public Tile(int y, int x, byte type)
: base(new Vector2(x * 32, y * 32))
{
this.x = x;
this.y = y;
switch (type)
{
case 1:
Color = Color.Gray;
this.type = TileType.Wall;
break;
case 2:
Color = Color.Blue;
this.type = TileType.Water;
break;
default:
this.type = TileType.Normal;
break;
}
}
}
Ajoutez ensuite une classe Map. Elle contiendra la liste des Tile à afficher dans un tableau
à deux dimensions. Ce tableau sera initialisé et rempli dans le constructeur de la classe
via un tableau de byte (0 pour une case normale, 1 pour un mur et 2 pour de l’eau). Bien
sûr, la classe dispose d’une méthode Draw() qui va parcourir le tableau et appeler la
méthode du même nom de chaque Tile. Pour terminer, la méthode ValidCoordinates ()
permettra de déterminer si une paire de coordonnées (x, y) correspond à un élément
valide du tableau.
La classe Map
class Map
{
Tile[,] tileList;
public Tile[,] TileList
{
get { return tileList; }
set { tileList = value; }
}
=Labat FM.book Page 192 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


192

public Map(byte[,] table)


{
tileList = new Tile[table.GetLength(0),table.GetLength(1)];
for (int y = 0; y < table.GetLength(0); y++)
{
for (int x = 0; x < table.GetLength(1); x++)
{
tileList[y, x] = new Tile(y, x, table[y, x]);
}
}
}
public void Draw(SpriteBatch spriteBatch)
{
foreach (Tile tile in tileList)
{
tile.Draw(spriteBatch);
}
}
public bool ValidCoordinates(int x, int y)
{
if (x < 0)
return false;
if (y < 0)
return false;
if (x >= tileList.GetLength(1))
return false;
if (y >= tileList.GetLength(0))
return false;
return true;
}
}

Lier les nœuds aux cases : la distance de Manhattan


La prochaine classe à ajouter est celle des nœuds, appelez-la Node. Chaque nœud doit être
lié à une case Tile, doit pouvoir avoir un nœud parent et enfin, et doit connaître l’estimation
de la distance vers le nœud de destination. Nous utiliserons ici la distance de Manhattan.
Dans ce calcul, n’oubliez pas d’ajouter le coût de la case (les valeurs étant fixées dans
l’énumération TileType). La méthode GetPossibleNode() va chercher les nœuds voisins
(uniquement sur les axes horizontaux et verticaux) qui sont des cases valides et ne sont
pas des murs. Elle renvoie ensuite une collection List<Node> contenant les nœuds ainsi
trouvés.
La classe Node
class Node
{
Tile tile;
=Labat FM.book Page 193 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
193

public Tile Tile


{
get { return tile; }
}
Node parent;
public Node Parent
{
get { return parent; }
set { parent = value; }
}
int estimatedMovement;
public int EstimatedMovement
{
get { return estimatedMovement; }
}
public Node(Tile tile, Node parent, Tile destination)
{
this.tile = tile;
this.parent = parent;
this.estimatedMovement = Math.Abs(tile.X - destination.X) + Math.Abs(tile.Y
➥ - destination.Y) + (int)tile.Type;
}
public List<Node> GetPossibleNode(Map map, Tile destination)
{
List<Node> result = new List<Node>();
// Bottom
if (map.ValidCoordinates(tile.X, tile.Y + 1) && map.TileList[tile.Y + 1,
➥ tile.X].Type != TileType.Wall)
result.Add(new Node(map.TileList[tile.Y + 1, tile.X], this,
➥ destination));
// Right
if (map.ValidCoordinates(tile.X + 1, tile.Y) && map.TileList[tile.Y,
➥ tile.X + 1].Type != TileType.Wall)
result.Add(new Node(map.TileList[tile.Y, tile.X + 1], this,
➥ destination));
// Top
if (map.ValidCoordinates(tile.X, tile.Y - 1) && map.TileList[tile.Y - 1,
➥ tile.X].Type != TileType.Wall)
result.Add(new Node(map.TileList[tile.Y - 1, tile.X], this,
➥ destination));
// Left
if (map.ValidCoordinates(tile.X - 1, tile.Y) && map.TileList[tile.Y,
➥ tile.X - 1].Type != TileType.Wall)
result.Add(new Node(map.TileList[tile.Y, tile.X - 1], this,
➥ destination));
return result;
}
}
=Labat FM.book Page 194 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


194

Listes ouverte et fermée : la collection List


Il ne reste plus qu’une chose à préparer : une collection qui servira aux listes ouverte et
fermée. Il s’agit ici de créer une collection dérivée de la collection List<T>, l’intérêt étant
de lui donner la possibilité de retourner un objet sans connaître son index, de savoir si un
nœud est déjà présent dans la liste et enfin, d’effectuer une insertion dichotomique dans
la liste.
Qu’est-ce qu’une insertion dichotomique ? Dans le principe de l’algorithme, vous avez
pu lire qu’à chaque itération, il faut choisir le nœud dont la distance avec le nœud de
destination est la plus faible. Vous pouvez donc parcourir à chaque fois la liste ouverte à
la recherche du nœud ayant la distance la plus faible ou alors entretenir une liste triée
dans laquelle vous savez que le premier élément de la liste est toujours celui qui a la
distance la plus faible. L’insertion dichotomique vous permet donc de placer cet élément
au bon endroit. Voici l’algorithme en pseudo-code :
Gauche <- 0 // Indice du premier élément
Droite <- Taille – 1 // Indice du dernier élément

TantQue Gauche <= Right


Faire

Centre <- (Gauche + Droite) / 2

Si distance du nœud à insérer < distance du nœud liste[centre]


// On peut se passer de toute la partie à droite de la liste
Droite = Centre - 1

Sinon Si distance du nœud à insérer > distance du nœud liste[centre]


// On peut se passer de toute la partie à gauche de la liste
Gauche = Centre + 1

Sinon
// On a trouvé le bon endroit pour l’insertion
Gauche = Centre
Arrêter l’algorithme

Fin Si

FinTantQue

Insérer le nœud à la position gauche


L’exemple de la figure 9-5 illustre l’insertion du chiffre 3 dans une liste triée contenant
les 8 autres premiers chiffres et le nombre 10. Il faut jouer avec les curseurs gauche et
droite jusqu’à en faire coïncider 1 avec le curseur centre. Les lignes contenant des lettres
correspondent à la position des curseurs, la ligne qui contient des chiffres en ordre croissant
correspond aux indices de la collection. Enfin, la ligne qui contient des chiffres en ordre
croissant, mais où il manque un chiffre, correspond aux valeurs de la collection.
=Labat FM.book Page 195 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
195

Figure 9-5
Insertion dichotomique du
chiffre 3 dans une liste

L’extrait de code suivant correspond à l’implémentation de ce que nous venons de voir.


La classe NodeList, liste générique personnalisée
class NodeList<T> : List<T> where T : Node
{
public new bool Contains(T node)
{
return this[node] != null;
}
public T this[T node]
{
get
{
int count = this.Count;
for (int i = 0; i < count; i++)
{
if (this[i].Tile == node.Tile)
return this[i];
}
return default(T);
}
}
public void DichotomicInsertion(T node)
{
int left = 0;
int right = this.Count - 1;
int center = 0;
while (left <= right)
{
center = (left + right) / 2;
if (node.EstimatedMovement < this[center].EstimatedMovement)
right = center - 1;
else if (node.EstimatedMovement > this[center].EstimatedMovement)
left = center + 1;
else
{
left = center;
break;
}
}
this.Insert(left, node);
}
}
=Labat FM.book Page 196 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


196

La classe Pathfinding : implémentation de l’algorithme


Il est temps de passer aux choses sérieuses ! Ajoutez une classe PathFinding au projet et
ajoutez-y une fonction statique qui retourne une liste de Tile. Elle devra recevoir en
argument la carte sur laquelle elle travaillera, ainsi que la case de départ et celle d’arrivée.
La première chose à faire est de déclarer tout ce qui sera utile dans la fonction. Tout d’abord
les collections : il en faut une qui contient la liste de cases pour la sortie de la fonction,
une pour la liste ouverte, une pour la liste fermée et une pour les nœuds voisins à analyser.
Notez enfin qu’une variable contenant le nombre d’éléments de cette dernière liste est
aussi déclarée. Il s’agit là d’une optimisation : l’utilisation d’une boucle for plutôt
qu’une boucle foreach ne requiert pas la création d’un objet pour l’énumération. Ensuite,
au lieu d’appeler à chaque fois la propriété Count, il est préférable de stocker sa valeur
dans une variable. L’optimisation peut être encore plus poussée en employant des tableaux
plutôt que des listes mais, pour ne pas compliquer plus les choses, ce n’est pas le cas ici.
Ensuite, il faut générer le nœud de départ (n’oubliez pas qu’il n’a pas de nœud parent) et
l’ajouter à la liste ouverte.
Le reste de la fonction se contente d’appliquer l’algorithme : retirez le nœud de la liste
ouverte et ajoutez-le à la liste fermée ; si la case du nœud est celle d’arrivée, remontez la
liste fermée et remplissez la liste de sortie de la fonction, sinon inspectez les nœuds
voisins. Si vous sortez de la boucle while, c’est qu’il n’y a plus d’éléments dans la liste
ouverte et qu’il n’existe donc aucune solution ; dans ce cas, retournez null.
La méthode de recherche du plus court chemin
class Pathfinding
{
public static List<Tile> CalculatePathWithAStar(Map map, Tile startTile, Tile
➥ endTile)
{
List<Tile> result = new List<Tile>();
NodeList<Node> openList = new NodeList<Node>();
NodeList<Node> closedList = new NodeList<Node>();
List<Node> possibleNodes;
int possibleNodesCount;

Node startNode = new Node(startTile, null, endTile);

openList.Add(startNode);

while (openList.Count > 0)


{
Node current = openList[0];
openList.RemoveAt(0);
closedList.Add(current);

if (current.Tile == endTile)
{
=Labat FM.book Page 197 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
197

List<Tile> sol = new List<Tile>();


while (current.Parent != null)
{
sol.Add(current.Tile);
current = current.Parent;
}
return sol;
}
possibleNodes = current.GetPossibleNode(map, endTile);
possibleNodesCount = possibleNodes.Count;
for (int i = 0; i < possibleNodesCount; i++)
{
if (!closedList.Contains(possibleNodes[i]))
{
if (openList.Contains(possibleNodes[i]))
{
if (possibleNodes[i].EstimatedMovement < openList
➥ [possibleNodes[i]].EstimatedMovement)
openList[possibleNodes[i]].Parent = current;
}
else
openList.DichotomicInsertion(possibleNodes[i]);
}
}
}
return null;
}
}

Phase de test
Il ne reste plus qu’à utiliser tout cela dans la classe ChapitreNeuf. Créez un objet de type
Map et initialisez-le via un tableau de byte. Dans la classe ci-dessous, la taille de la fenêtre
s’adapte automatiquement à la taille de la carte. Pour cela, il suffit de multiplier la taille
de chaque dimension de la carte par 32 (taille d’une case).
Dans la méthode Initialize(), vous allez créer un objet Tile pour le point de départ et un
autre pour le point d’arrivée. Modifiez la couleur du point de départ afin qu’il soit plus
facilement reconnaissable, stockez le résultat de CalculatePathWithAStar() dans une liste
de cases et parcourez-la de manière à coloriser toutes les cases qui composent le chemin.
Le premier élément de cette liste est le point d’arrivée, vous pouvez donc employer une
couleur différente. Enfin, il n’est pas nécessaire de revenir sur le contenu des méthodes
LoadContent() et Draw()…
Une classe pour tester l’algorithme
public class ChapitreNeuf : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
=Labat FM.book Page 198 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


198

Map map;
public ChapitreNeuf()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
map = new Map(new byte[,] {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0},
{1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 2, 0, 1, 0, 1, 0},
{0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 0, 1, 1},
{0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1},
{0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1},
{0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1},
{0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}
});
graphics.PreferredBackBufferWidth = map.TileList.GetLength(1) * 32;
graphics.PreferredBackBufferHeight = map.TileList.GetLength(0) * 32;
}
protected override void Initialize()
{
Tile start = map.TileList[13, 0];
Tile end = map.TileList[11, 15];
start.Color = Color.Green;
end.Color = Color.Red;
List<Tile> liste = Pathfinding.CalculatePathWithAStar(map, start, end);
if (liste != null)
{
liste[0].Color = Color.Red;
for (int i = 1; i < liste.Count; i++)
liste[i].Color = Color.Orange;
}
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
foreach (Tile tile in map.TileList)
{
tile.LoadContent(Content, "tile");
}
}
protected override void Update(GameTime gameTime)
{
=Labat FM.book Page 199 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
199

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
map.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
Vous pouvez à présent tester l’algorithme sous toutes ses coutures. Commencez par
bloquer le passage pour constater qu’il ne retourne bien aucun chemin.
Vous pouvez ajouter autant de types de case que votre imagination vous le permet. Par
exemple, sur la figure 9-6, vous remarquez un bosquet en vert clair (le coût de chacune de
ces cases est de un), ce qui amène l’algorithme à passer de préférence par la rivière.

Figure 9-6
Test de l’algorithme avec un
nouveau type de case

Enfin, vous pouvez aussi essayer une carte sur laquelle A* n’est pas performant (figure 9-7).
L’algorithme a su trouver la bonne solution, toutefois il est tombé dans le piège tendu par
la carte et a dû analyser toutes les cases de celle-ci.
=Labat FM.book Page 200 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


200

Figure 9-7
Test de l’algorithme avec une carte piège

Certes, l’algorithme A* est rapide, mais il reste encore de nombreuses choses à ajouter,
notamment :
• Sur des grandes cartes, diviser le parcours en check points et calculer seulement le
temps pour aller du point de départ au premier check point, puis du premier check
point au second, etc.
• Gérer les collisions avec d’autres entités différentes d’un mur ou d’un relief, ce principe
étant applicable grâce à la notion de coût des nœuds.
Bien évidemment, toutes ces petites idées induiront d’autres problèmes, notamment de
performances. Le code source que nous venons d’étudier n’est pas parfait : c’est à vous
de développer et d’optimiser sans cesse le vôtre en fonction des besoins.

Cas pratique : implémenter le déplacement d’un personnage


sur une carte
La dernière partie de ce chapitre est consacrée à la réalisation d’une application exemple
plus poussée qui utilise le pathfinding. Le but est de déplacer un personnage (représenté
par un carré) là où l’utilisateur cliquera sur la carte.

Préparation : identifier et traduire les actions du joueur


Tout d’abord, vous allez devoir vous occuper de la souris. Par défaut, elle est masquée
dans une application sous XNA. Cependant, vous pouvez l’afficher simplement grâce à
la propriété IsMouseVisible.
IsMouseVisible = true;
Ensuite, vous devez être en mesure de savoir si l’utilisateur vient de presser le bouton
gauche de la souris et de récupérer les coordonnées du pointeur à ce moment-là.
Commencez par ajouter la classe ServiceHelper que vous avez développée dans un chapitre
précédent. Comme pour le clavier, commencez par créer une interface IMouseService.
=Labat FM.book Page 201 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
201

interface IMouseService
{
bool LeftButtonHasBeenPressed();
Vector2 GetCoordinates();
}
Il vous reste ensuite à créer la classe qui implantera cette interface. N’oubliez pas d’ajouter
dans le constructeur l’instance de la classe à la collection du ServiceHelper. Vous pouvez
déterminer si le joueur a pressé un bouton ou non en regardant l’état de ce dernier aux
instants t et t-1. À chaque appel de Update(), vous devrez stocker l’état de la souris et
archiver son ancien état.
Le service qui met à disposition l’état de la souris
public class MouseService : GameComponent, IMouseService
{
MouseState mouseState = Mouse.GetState();
MouseState lastMouseState;
public MouseService(Game game)
: base(game)
{
ServiceHelper.Add<IMouseService>(this);
}
public bool LeftButtonHasBeenPressed()
{
return mouseState.LeftButton == ButtonState.Released &&
➥ lastMouseState.LeftButton == ButtonState.Pressed;
}
public Vector2 GetCoordinates()
{
return new Vector2(mouseState.X, mouseState.Y);
}
public override void Update(GameTime gameTime)
{
lastMouseState = mouseState;
mouseState = Mouse.GetState();
}
}

Créer le personnage
Modifiez la classe Tile de manière à ajouter un nouveau type correspondant à un person-
nage. Dans l’exemple suivant, les personnages seront affichés en noir.
La classe Tile améliorée
enum TileType
{
Wall = -1,
Normal = 0,
=Labat FM.book Page 202 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


202

Tree = 1,
Water = 2,
Human = -1,
};

class Tile : Sprite


{
int x;

public int X
{
get { return this.x; }
set { this.x = value; }
}

int y;

public int Y
{
get { return this.y; }
set { this.y = value; }
}

TileType type;

public TileType Type


{
get { return type; }
}

public Tile(int y, int x, byte type)


: base(new Vector2(x * 32, y * 32))
{
this.x = x;
this.y = y;

switch (type)
{
case 1:
Color = Color.Gray;
this.type = TileType.Wall;
break;
case 3:
Color = Color.LightGreen;
this.type = TileType.Tree;
break;
case 2:
Color = Color.Blue;
this.type = TileType.Water;
break;
=Labat FM.book Page 203 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
203

case 4:
Color = Color.Black;
this.type = TileType.Human;
break;
default:
this.type = TileType.Normal;
break;
}
}
}
Enfin, créez une classe dérivée de Tile que vous appelerez Hero. Celle-ci contiendra un
champ msElapsed qui permettra de réguler la vitesse de déplacement du personnage.
L’autre champ est une liste de Tile ; une propriété permet de le définir et vérifie au
passage que la liste de cases transmise n’est pas nulle. Dans la méthode Update(), vous
déplacerez la position du Hero à l’écran, ainsi que sa position sur la carte en fonction de
celles du dernier élément de la liste, puis vous supprimerez ce dernier élément.
La classe Hero représente un élément particulier de la carte
class Hero : Tile
{
int msElapsed = 0;

List<Tile> walkingList = new List<Tile>();

public List<Tile> WalkingList


{
set { if (value != null) walkingList = value; }
}

public Hero(int y, int x, byte type)


: base(y, x, type)
{
}

public void Update(GameTime gameTime)


{
msElapsed += gameTime.ElapsedGameTime.Milliseconds;

if (walkingList.Count != 0)
{
if (msElapsed >= 100)
{
msElapsed = 0;
X = walkingList[walkingList.Count - 1].X;
Y = walkingList[walkingList.Count - 1].Y;
Position = walkingList[walkingList.Count - 1].Position;
walkingList.RemoveAt(walkingList.Count - 1);
}
}
}
}
=Labat FM.book Page 204 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


204

Implémenter l’algorithme
Il ne vous reste plus qu’à utiliser tous ces nouveaux éléments. Instanciez un objet de type
Hero et, lorsque l’utilisateur clique quelque part sur l’écran, passez à cet objet le résultat
du calcul du chemin. Si le joueur clique sur le personnage, il n’est pas nécessaire d’effectuer
le calcul du chemin !

Figure 9-8
Le sprite noir se déplace
à la perfection

La nouvelle classe de test de l’algorithme


public class ChapitreNeuf : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Map map;
Hero heros;

public ChapitreNeuf()
{
graphics = new GraphicsDeviceManager(this);
ServiceHelper.Game = this;
Components.Add(new MouseService(this));
Content.RootDirectory = "Content";

map = new Map(new byte[,] {


{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 2, 1, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 1, 0},
{1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 0, 0, 1, 0, 1, 0},
=Labat FM.book Page 205 Vendredi, 19. juin 2009 4:01 16

Pathfinding : programmer les déplacements des personnages


CHAPITRE 9
205

{0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 0, 1, 1},
{0, 0, 0, 1, 0, 1, 0, 0, 3, 3, 0, 1, 2, 0, 0, 0, 0, 1},
{0, 0, 0, 1, 0, 1, 1, 3, 3, 1, 1, 1, 2, 0, 1, 0, 0, 1},
{0, 0, 0, 1, 0, 0, 1, 3, 3, 3, 3, 2, 2, 0, 1, 1, 0, 1},
{0, 0, 0, 1, 0, 0, 1, 3, 3, 3, 3, 2, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 3, 2, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 3, 3, 1, 1, 3, 2, 0, 0, 0, 0, 0, 0}
});

graphics.PreferredBackBufferWidth = map.TileList.GetLength(1) * 32;


graphics.PreferredBackBufferHeight = map.TileList.GetLength(0) * 32;
IsMouseVisible = true;
}

protected override void Initialize()


{
heros = new Hero(13, 0, 4);

base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);

foreach (Tile tile in map.TileList)


{
tile.LoadContent(Content, "tile");
}
heros.LoadContent(Content, "tile");
}

protected override void Update(GameTime gameTime)


{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

if (ServiceHelper.Get<IMouseService>().LeftButtonHasBeenPressed())
{
if (heros.X != ((int)ServiceHelper.Get<IMouseService>()
➥ .GetCoordinates().X / 32) || heros.Y != ((int)ServiceHelper
➥ .Get<IMouseService>().GetCoordinates().Y / 32))
heros.WalkingList = Pathfinding.CalculatePathWithAStar(map, heros, map
➥ .TileList[(int)ServiceHelper.Get<IMouseService>().GetCoordinates().Y
➥ / 32,(int)ServiceHelper.Get<IMouseService>().GetCoordinates().X / 32]);
}

heros.Update(gameTime);
base.Update(gameTime);
}
=Labat FM.book Page 206 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


206

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
map.Draw(spriteBatch);
heros.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
Dans l’extrait de code précédent, nous créons une carte et nous y ajoutons un personnage.
Si vous exécutez maintenant le projet, vous constaterez que le personnage se déplace là
où vous cliquez.

En résumé
Ce chapitre vous a présenté rapidement les techniques propres à l’intelligence artificielle.
Vous avez découvert l’algorithme de recherche du plus court chemin : A*. Vous avez
appris à développer cet algorithme en C# et à l’appliquer à un exemple de jeu vidéo.
=Labat FM.book Page 207 Vendredi, 19. juin 2009 4:01 16

10
Collisions et physique :
créer un simulateur
de vaisseau spatial

Vous commencez à connaître bon nombre des aspects du framework XNA et vous vous
êtes sûrement déjà aventuré dans la création d’un vrai jeu. Vous avez alors certainement
été confronté aux problèmes liés aux règles physiques de l’univers que vous avez créé,
surtout si ce jeu était un clone d’Asteroids, ou tout du moins un jeu y ressemblant.

Culture Asteroids
Il s’agit d’un jeu de tir spatial en deux dimensions qui a fait sensation en 1979 (et qui continue sûrement à
remporter du succès auprès de joueurs nostalgiques de ce temps). Dans ce jeu, vous contrôlez un vaisseau
qui doit faire face, armé de son lance-missiles, à des champs d’astéroïdes et des vaisseaux ennemis. Le
vaisseau que le joueur dirige connaît une inertie évoquant le milieu spatial, ce qui rend sa prise en main
délicate.

Dans ce chapitre consacré à la création d’un petit jeu de simulation spatiale en 2D, notre
but est d’étudier les collisions par rectangle ou par pixels ainsi qu’un moteur physique (le
module d’un jeu chargé du déplacement de diverses entités), FarseerPhysics.

Comment détecter les collisions


Dans la première partie de ce chapitre, vous allez réaliser une première version squelette
du projet, le but étant de s’intéresser tout particulièrement à la gestion des collisions entre
=Labat FM.book Page 208 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


208

le vaisseau du joueur et les astéroïdes. Dans un premier temps, la gestion se basera sur la
collision entre rectangles, puis nous descendrons au niveau des pixels.

Créer les bases du jeu


Commencez par créer un projet vierge. Avant toute chose, ajoutez deux variables statiques
à la classe principale du projet. Ces dernières définiront la taille de la fenêtre, il faudra
donc les appliquer à l’objet graphics. Les valeurs qui sont utilisées ici correspondent à
une fenêtre étroite, mais assez haute.
public static int SCREEN_WIDTH = 512;
public static int SCREEN_HEIGHT = 748;

public ChapitreDix()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferHeight = SCREEN_HEIGHT;
graphics.PreferredBackBufferWidth = SCREEN_WIDTH;
}

Créer le vaisseau
Maintenant, occupez-vous de la création du vaisseau du joueur. En espérant que cela ne
froissera pas les fans du jeu d’origine, dans l’exemple proposé dans ce livre, le joueur
contrôlera un simple triangle blanc de 32 pixels par 32 pixels, visible à la figure 10-1.

Figure 10-1
Quel ennemi oserait se présenter face à pareil engin ?

Comme d’habitude, importez les fichiers suivants des précédents projets : IKeyboard
Service.cs, KeyboardService.cs, ServiceHelper.cs et Sprite.cs. Ajoutez une classe Player
au projet ; vous la ferez dériver de la classe Sprite. Puisque la position du vaisseau du
joueur ne peut pas être déterminée dès l’appel au constructeur (il faut connaître la taille
de la texture pour pouvoir centrer le vaisseau), passez plutôt la valeur prédéfinie Vector2
.Zero au constructeur de la classe parente.
public Player()
: base(Vector2.Zero)
{
}
L’étape suivante est de compléter la méthode Update(). Celle-ci aura deux tâches à effectuer :
• La première est de déplacer le vaisseau en fonction des touches que le joueur presse :
rien de plus facile en appelant le service dédié. Profitez-en pour ajouter un élément de
gameplay déterminant : la vitesse du vaisseau lorsqu’il recule sera moins importante
que lorsqu’il avance.
=Labat FM.book Page 209 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
209

• La seconde tâche consiste à vérifier que le vaisseau du joueur ne quitte pas l’écran. Si
cela se produit, il a perdu, il faut le replacer au centre de l’écran.
Vous serez en mesure de répondre à ces exigences en utilisant les propriétés de
taille de la texture utilisée par le vaisseau, ainsi que la taille de l’écran (les variables
statiques SCREEN_WIDTH et SCREEN_HEIGHT). Comme d’habitude, n’oubliez pas que, par
défaut, le point d’origine est placé en haut à gauche de la texture.

La méthode Update() complétée


public void Update(GameTime gameTime)
{
if(ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Up))
Position = new Vector2(Position.X, Position.Y - (float)(0.4 *
➥ gameTime.ElapsedGameTime.Milliseconds));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Down))
Position = new Vector2(Position.X, Position.Y + (float)(0.2 *
➥ gameTime.ElapsedGameTime.Milliseconds));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Left))
Position = new Vector2(Position.X - (float)(0.3 *
➥ gameTime.ElapsedGameTime.Milliseconds), Position.Y);

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Right))
Position = new Vector2(Position.X + (float)(0.3 *
➥ gameTime.ElapsedGameTime.Milliseconds), Position.Y);

if (Position.X < 0)
Position = new Vector2(0, Position.Y);

if (Position.X + Texture.Width > ChapitreDix.SCREEN_WIDTH)


Position = new Vector2(ChapitreDix.SCREEN_WIDTH - Texture.Width,
➥ Position.Y);

if (Position.Y < 0)
Position = new Vector2(Position.X, 0);

if (Position.Y + Texture.Height > ChapitreDix.SCREEN_HEIGHT)


Position = new Vector2(Position.X, ChapitreDix.SCREEN_HEIGHT -
➥ Texture.Height);
}

À vos claviers
La structure qui a été retenue pour ce jeu se contente d’utiliser des classes dérivées de la classe Sprite.
Cependant, vous pourriez très bien faire dériver la classe Player de la classe DrawableGameComponent,
ou encore utiliser les interfaces IGameComponent, IUpdatable et IDrawable. N’hésitez pas à récrire ce
mini-jeu exemple avec ces solutions, cela constitue un très bon entraînement.
=Labat FM.book Page 210 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


210

Dernière méthode de cette classe, ResetPosition () se chargera de placer le vaisseau du


joueur au centre de la largeur de l’écran et en retrait de 10 % de sa hauteur. Cette
méthode sera appelée en début de partie (après le chargement de la texture du vaisseau),
ainsi qu’à chaque fois que le joueur aura perdu.
public void ResetPosition()
{
Position = new Vector2(ChapitreDix.SCREEN_WIDTH / 2 - (Texture.Width / 2), 9 *
➥ (ChapitreDix.SCREEN_HEIGHT / 10) - Texture.Height);
}

Créer les astéroïdes


À présent, vous allez vous occuper des astéroïdes. Tout d’abord, créez une texture ou
récupérez-la sur Internet, par exemple sur http://www.cgtextures.com/. Comme vous le
voyez à la figure 10-2, nous utilisons un simple disque marron.
Figure 10-2
Des sphères parfaites se cachent peut-être dans l’espace…

Créez une classe Asteroid et ajoutez-lui un attribut speed de type Vector2 : il pourra y
avoir plusieurs astéroïdes à l’écran, la position et la vitesse de chacun d’entre eux sera
aléatoire. Vous fixez ces deux éléments dans une méthode privée Initialize (). Pour la
génération de nombres aléatoires, vous utiliserez un objet statique de la classe Random et
vous passerez à son constructeur le nombre de millisecondes à cet instant.
static Random random = new Random(DateTime.Now.Millisecond);

Avancé. Les nombres aléatoires


Il faut savoir qu’en informatique, les nombres aléatoires ne le sont pas réellement. En fait, il est impossible de
générer une suite de nombres réellement aléatoires, il faudrait plutôt les appeler nombres pseudo-aléatoires.
La détermination de ces nombres se fait via l’utilisation d’une graine, c’est-à-dire d’une valeur de départ.
Si la valeur de cette graine est toujours la même, les nombres générés par le programme seront toujours
les mêmes. Pour éviter ce problème, vous pouvez faire varier la valeur de la graine en fonction du temps.

La méthode Next() de l’objet random attend comme paramètres une borne inférieure et une
borne supérieure, puis retourne un entier. Initialement, le sprite devra se situer légèrement
au-dessus de la fenêtre et n’importe où sur l’axe des abcisses. Pour la vitesse, en ce qui
concerne l’axe X, l’astéroïde doit pouvoir aller vers la gauche comme vers la droite ; en
ce qui concerne l’axe Y, il doit uniquement pouvoir aller vers le bas.
private void Initialize()
{
Position = new Vector2(random.Next(0, ChapitreDix.SCREEN_WIDTH - Texture.Width),
➥ -Texture.Height);
speed = new Vector2((float)random.Next(-7, 7) / 10, (float)random.Next(1, 7)
➥ / 10);
}
=Labat FM.book Page 211 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
211

Vous pouvez à présent écrire le constructeur. Dans celui-ci, vous appellerez la méthode
LoadContent () de la classe parente, ainsi que la méthode Initialize ().
public Asteroid(ContentManager content)
: base(Vector2.Zero)
{
base.LoadContent(content, "asteroid");
Initialize();
}
Dernière chose, la méthode Update () où vous mettrez à jour la position de l’astéroïde et,
s’il sort de l’écran, où vous appellerez sa méthode Initialize().
public void Update(GameTime gameTime)
{
Position = new Vector2(Position.X + (speed.X *
➥ gameTime.ElapsedGameTime.Milliseconds), Position.Y + (speed.Y *
➥ gameTime.ElapsedGameTime.Milliseconds));

if (Position.X + Texture.Width < 0)


Initialize();

if (Position.X > ChapitreDix.SCREEN_WIDTH)


Initialize();

if (Position.Y > ChapitreDix.SCREEN_HEIGHT)


Initialize();
}
Vous n’avez plus qu’à utiliser ces deux classes. Il n’y a rien de spécial à souligner en ce
qui concerne la création du vaisseau du joueur. Vous stockerez les astéroïdes dans une
collection List<>. Pour faire apparaître un astéroïde toutes les x secondes, utilisez un objet
de type TimeSpan que vous remettrez à zéro à chaque ajout d’un nouvel objet à la liste.
La classe de base du jeu complétée
public class ChapitreDix : Microsoft.Xna.Framework.Game
{
public static int SCREEN_WIDTH = 512;
public static int SCREEN_HEIGHT = 748;
static int NEW_METEOR_TIME = 5;

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Player playerShip;
List<Asteroid> asteroids = new List<Asteroid>();

TimeSpan elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;

public ChapitreDix()
{
=Labat FM.book Page 212 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


212

graphics = new GraphicsDeviceManager(this);


Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferHeight = SCREEN_HEIGHT;
graphics.PreferredBackBufferWidth = SCREEN_WIDTH;
}

protected override void Initialize()


{
playerShip = new Player();

base.Initialize();
}

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);

playerShip.LoadContent(Content, "ship");
playerShip.ResetPosition();
}

protected override void Update(GameTime gameTime)


{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

playerShip.Update(gameTime);

foreach (Asteroid asteroid in asteroids)


asteroid.Update(gameTime);

elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime;

if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME)


{
asteroids.Add(new Asteroid(Content));
elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;
}

base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
playerShip.Draw(spriteBatch);
foreach (Asteroid asteroid in asteroids)
=Labat FM.book Page 213 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
213

asteroid.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}
Vous pouvez à présent compiler et exécuter le jeu (voir figure 10-3). Cependant, sans les
collisions, ce n’est pas très intéressant d’y jouer…

Figure 10-3
Les astéroïdes deviennent
vite très nombreux

Établir une zone de collision autour des astéroïdes


La méthode la plus simple pour tester si le vaisseau du joueur entre en collision avec un
astéroïde consiste à encadrer chaque élément par un rectangle, puis à tester si les rectangles
se coupent ou non.
Commencez par ajouter une propriété à la classe Sprite afin que les classes Player et
Asteroid puissent en bénéficier. Cette propriété devra créer un rectangle en fonction de
la position du Sprite et de la taille de sa texture. Pour que la classe reste générique, si la
variable sourceRectangle n’est pas nulle, utilisez-la plutôt que les dimensions de la
texture.
=Labat FM.book Page 214 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


214

public Rectangle Rectangle


{
get
{
if (sourceRectangle == null)
return new Rectangle((int)position.X, (int)position.Y, texture.Width,
➥ texture.Height);
else
return new Rectangle((int)position.X, (int)position.Y,
➥ sourceRectangle.Value.Width, sourceRectangle.Value.Height);
}
}
Cette portion de code aurait aussi pu s’écrire de la manière suivante :
public Rectangle Rectangle
{
get { return new Rectangle((int)Position.X, (int)Position.Y,
(sourceRectangle == null) ? Texture.Width : sourceRectangle.Value.Width,
➥ (sourceRectangle == null) ? Texture.Height : sourceRectangle.Value.Height);
}
}

Avancé Opérateur ternaire


L’exemple précédent utilise un élément du langage que vous n’avez encore jamais vu : l’opérateur ternaire.
Il permet de résumer des blocs if en une seule ligne. Sa syntaxe est la suivante :
(test) ? valeur si vrai : valeur si faux

Modifiez ensuite la méthode Update () de la classe ChapitreDix pour qu’elle effectue le


test entre les deux rectangles. Utilisez la méthode Intersects() d’un rectangle et passez
l’autre rectangle en argument.
Si les deux rectangles se superposent, le joueur a perdu : il faut réinitialiser sa position et
vider la liste des astéroïdes. La liste se vide via la méthode Clear (). Attention, si vous
supprimez un ou plusieurs éléments de la collection, vous devez sortir de la boucle qui
est en train de l’énumérer.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
playerShip.Update(gameTime);
foreach (Asteroid asteroid in asteroids)
{
asteroid.Update(gameTime);
if (playerShip.Rectangle.Intersects(asteroid.Rectangle))
{
playerShip.ResetPosition();
=Labat FM.book Page 215 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
215

asteroids.Clear();
break;
}
}
elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime;
if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME)
{
asteroids.Add(new Asteroid(Content));
elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;
}
base.Update(gameTime);
}
Vous pouvez tester le jeu à présent : la détection des collisions fonctionne et le jeu
reprendra à zéro dès que le joueur percutera un astéroïde. En réalité, cette dernière phrase
n’est pas juste : le jeu reprend à zéro lorsque le rectangle qui entoure le vaisseau du
joueur entre en collision avec celui qui entoure un astéroïde, pourtant il n’y a pas forcément
collision entre les deux éléments (figure 10-4).

Figure 10-4
Il y a collision entre les rectangles mais pas
entre les deux éléments

Comment savoir s’il y a vraiment collision entre deux éléments ? La réponse se situe au
niveau des pixels… Si vous détectez une collision potentielle grâce à la méthode d’inter-
section entre les rectangles, vous devrez zoomer sur la zone de chevauchement entre les
deux sprites et analyser les pixels de cette zone. Si, à un point (x, y), les pixels des deux
sprites sont totalement opaques, il y a collision.
Vous pouvez récupérer des informations sur la couleur de chaque pixel d’une texture
grâce à la méthode GetData () de la classe Texture2D. Commencez donc par ajouter une
nouvelle propriété à la classe Sprite qui permettra de récupérer ces informations. Passez
en paramètre à la méthode GetData() un tableau de Color qu’elle devra compléter. Le
nombre d’éléments du tableau sera le même que le nombre de pixels dans la texture.
public Color[] TextureData
{
get
{
Color[] textureData = new Color[texture.Width * texture.Height];
texture.GetData(textureData);
return textureData;
}
}
=Labat FM.book Page 216 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


216

Passons maintenant à l’écriture de la méthode CollisionPerPixel (dans le projet exemple,


elle est rattachée à la classe ChapitreDix). La première chose à faire est de déterminer le
rectangle contenant tous les pixels concernés par la collision entre les deux sprites. Une
fois la position des quatre côtés du rectangle déterminée, utilisez-les pour parcourir le
tableau de pixels. Attention, tous les pixels sont classés de manière linéaire dans le
tableau puisque celui-ci ne comporte qu’une dimension.
Pour chacun des pixels de cette zone, vérifiez la composante alpha (la transparence). Si elle
n’est pas nulle pour les deux sprites, alors les deux sprites ne sont pas totalement transparents
et il y a collision.
protected bool CollisionPerPixels(Sprite spriteA, Sprite spriteB)
{
int top = Math.Max(spriteA.Rectangle.Top, spriteB.Rectangle.Top);
int bottom = Math.Min(spriteA.Rectangle.Bottom, spriteB.Rectangle.Bottom);
int left = Math.Max(spriteA.Rectangle.Left, spriteB.Rectangle.Left);
int right = Math.Min(spriteA.Rectangle.Right, spriteB.Rectangle.Right);
for (int y = top; y < bottom; y++)
{
for (int x = left; x < right; x++)
{
Color colorA = spriteA.TextureData[(x - spriteA.Rectangle.Left) +
(y - spriteA.Rectangle.Top) * spriteA.Rectangle.Width];
Color colorB = spriteB.TextureData[(x - spriteB.Rectangle.Left) +
(y - spriteB.Rectangle.Top) * spriteB.Rectangle.Width];
if (colorA.A != 0 && colorB.A != 0)
return true;
}
}
return false;
}
Reprenez la méthode et ajoutez l’appel à la fonction CollisionPerPixels() en plus de la
méthode de détection qui utilise les rectangles.

Langage C# Opérateur &&


Lorsque vous utilisez l’opérateur && de la manière suivante :
test_A && test_B
Si le résultat de test_A est faux, test_B ne sera même pas exécuté. Ainsi, le test qui descend au niveau
de détails des pixels ne sera exécuté que si le test plus large avec les rectangles a détecté une collision
potentielle.

protected override void Update(GameTime gameTime)


{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
=Labat FM.book Page 217 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
217

playerShip.Update(gameTime);
foreach (Asteroid asteroid in asteroids)
{
asteroid.Update(gameTime);
if (playerShip.Rectangle.Intersects(asteroid.Rectangle) &&
➥ CollisionPerPixels(playerShip, asteroid))
{
playerShip.ResetPosition();
asteroids.Clear();
break;
}
}
elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime;
if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME)
{
asteroids.Add(new Asteroid(Content));
elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;
}
base.Update(gameTime);
}
Cette fois-ci, si vous testez le jeu, vous remarquez qu’il est possible d’effleurer les astéroïdes
sans problèmes de collisions (figure 10-5). Il ne vous reste plus qu’à compléter le jeu en
ajoutant par exemple un système de gestion du score.
Figure 10-5
La détection de collision est maintenant plus précise

Simuler un environnement spatial : la gestion de la physique


Votre simulation spatiale n’est pas encore parfaite : en effet, les déplacements des vaisseaux
ne semblent pas vraiment naturels et les astéroïdes n’entrent pas en collision entre eux…
Dans la deuxième partie de ce chapitre, vous allez apprendre à utiliser un moteur physique
pour simuler un environnement ressemblant à l’espace.

Choisir un moteur physique


En fait, le moteur physique est le module du jeu qui s’occupe du mouvement des objets, de la
manière dont ils interagissent les uns avec les autres (par exemple les collisions), ou encore
des comportements spéciaux qu’ils peuvent avoir (rebonds, frictions, déformations, etc.).
Certains jeux portent un intérêt énorme à la physique. Par exemple, dans Half Life 2, ou
encore dans Portal (ces deux jeux utilisent le même moteur physique Havok), le joueur
=Labat FM.book Page 218 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


218

est très souvent confronté à des énigmes qu’il devra résoudre en utilisant les lois de la
physique. Bien sûr un moteur physique peut être beaucoup plus modeste, comme ceux
qui sont utiles dans les petits jeux de plates-formes Mario-like, où vous devez simplement
vous contenter d’appliquer la gravité sur vos personnages.
Le projet Phun (http://www.phunland.com/wiki/Home), à mi-chemin entre le jeu et l’utilitaire,
est un formidable simulateur physique issu des recherches d’une université suédoise.
Vous y dessinez des formes auxquelles vous pouvez attribuer densité, poids, etc. Même
les liquides y sont gérés (figure 10-6) !

Figure 10-6
Les possibilités de Phun vous occuperont pendant des heures

Jusqu’à présent, toutes les briques nécessaires à la création d’un jeu vidéo (gestion de
l’affichage à l’écran, des périphériques, du son, etc.) vous étaient fournies par XNA.
Cependant, XNA ne possède nativement aucun système pour la gestion de la physique,
vous allez devoir en créer un vous-même… Ou, plus modestement, en utiliser un fourni
par un autre développeur.
Il existe un grand nombre de moteurs physiques sur Internet. La première chose à faire
est d’en choisir un utilisable en C#, ensuite il faut s’intéresser à sa licence d’utilisation,
par exemple :
• gratuit et open source (vous pouvez accéder à leur code source et y apporter des modi-
fications) dans tous les contextes ;
=Labat FM.book Page 219 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
219

• gratuit et open source uniquement pour les jeux non commerciaux ;


• gratuit et closed source dans tous les contextes ;
• payant et closed source.

Licences
Il existe un tas d’autres licences (elles portent d’ailleurs toutes un nom), les nuances entre elles étant
parfois très subtiles : identifiez donc bien votre besoin lors du choix d’un moteur.

Dans ce chapitre, vous allez faire vos premiers pas avec le moteur FarseerPhysics. Ce
moteur est gratuit, open source et directement compatible avec XNA.

Télécharger et installer FarseerPhysics


Le moteur physique FarseerPhysics est, au moment de l’écriture de ce livre, disponible
en version 2.0.1. Le projet a été initialement lancé par Jeff Weber, mais il est à présent
maintenu par une équipe de trois personnes. Il est disponible pour XNA et Silverlight (la
technologie Microsoft concurrente d’Adobe Flash), mais propose aussi des classes
indépendantes de toute plate-forme graphique.
1. Rendez-vous sur la page du projet sur CodePlex, la plate-forme de Microsoft pour les
projets open source : http://www.codeplex.com/FarseerPhysics. Accédez à la page de téléchar-
gement en cliquant sur l’onglet Releases et choisissez le projet pour XNA (figure 10-7).

Figure 10-7
La version du moteur adaptée
à une utilisation avec XNA

2. Les développeurs proposent aussi une version du moteur avec des exemples d’utili-
sation simples ou plus avancés. Vous pouvez choisir de télécharger ces versions pour
tester les capacités du moteur (figures 10-8 et 10-9).
=Labat FM.book Page 220 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


220

Figure 10-8
Une grande quantité de cubes soumis à la gravité

Figure 10-9
Le moteur sait gérer une multitude de collisions sans que les performances en pâtissent trop
=Labat FM.book Page 221 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
221

3. Que vous ayez choisi de télécharger le moteur avec ou sans exemples, l’archive
contiendra un projet FarseerPhysics.csproj. Ajoutez ce projet à la solution (figure 10-10)
et générez-le (figure 10-11).

Figure 10-10
Ajout du projet de FarseerPhysics à la solution

Figure 10-11
Génération du projet
FarseerPhysics

4. Créez ensuite un projet nommé ChapitreDix_2. Pour pouvoir utiliser le moteur dans
ce projet, vous devez ajouter une référence vers le projet FarseerPhysics. Cliquez
droit sur la section référence du projet dans l’explorateur de solution, puis cliquez
sur Ajouter une référence… Dans la fenêtre qui s’ouvre, cliquez sur l’onglet Projets
et choisissez dans la liste celui nommé FarseerPhysics (figure 10-12).

Figure 10-12
Ajout d’une référence vers un autre projet
=Labat FM.book Page 222 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


222

5. La dernière chose à faire est d’ajouter une directive using.


using FarseerGames.FarseerPhysics;

Prise en main du moteur physique


Une nouvelle fois, ajoutez au projet les fichiers IKeyboardService.cs, KeyboardService.cs,
ServiceHelper.cs et Sprite.cs. Créez les classes Player et Asteroid qui dériveront de la
classe Sprite.
L’utilisation du moteur FarseerPhysics repose sur la classe PhysicsSimulator qui dispose
de deux constructeurs : le premier n’attend aucun paramètre, alors que le second attend
un objet de type Vector2. Cet objet correspond à la gravité à appliquer sur les axes X et Y.
Donc, si vous utilisez le constructeur sans arguments, il n’y aura pas de gravité !
Vous devrez appeler régulièrement la méthode Update() du simulateur pour que celui-ci
mette à jour tous les éléments qu’il gère. Cette méthode attend comme paramètre un inter-
valle de temps. Pour se rapprocher des calculs physiques habituels, passez-lui un temps
en secondes. Ce temps est récupérable en utilisant la propriété ElapsedGameTime.Milliseconds
que vous diviserez par mille (la propriété ElapsedGameTime.Seconds étant un entier, elle
serait imprécise).
Enfin, passez l’objet de type PhysicsSimulator au constructeur des classes Player et
Asteroid. Ci-dessous, vous retrouvez le code complet de la classe ChapitreDix_2.
Première classe de test du moteur physique
public class ChapitreDix_2 : Microsoft.Xna.Framework.Game
{
public static int SCREEN_WIDTH = 512;
public static int SCREEN_HEIGHT = 748;
static int NEW_ASTEROID_TIME = 5;
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
PhysicsSimulator physicsSimulator;
Player ship;
List<Asteroid> asteroids = new List<Asteroid>();
TimeSpan elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;
public ChapitreDix_2()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferHeight = SCREEN_HEIGHT;
graphics.PreferredBackBufferWidth = SCREEN_WIDTH;
}
=Labat FM.book Page 223 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
223

protected override void Initialize()


{
physicsSimulator = new PhysicsSimulator();
ship = new Player(physicsSimulator);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
ship.LoadContent(Content, "ship2");
ship.ResetPosition();
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
physicsSimulator.Update(gameTime.ElapsedGameTime.Seconds * 0.001f);
ship.Update(gameTime);
foreach (Asteroid asteroid in asteroids)
{
asteroid.Update(gameTime);
}
elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime;
if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_ASTEROID_TIME)
{
asteroids.Add(new Asteroid(physicsSimulator, Content));
elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero;
}

base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
ship.Draw(spriteBatch);
foreach (Asteroid asteroid in asteroids)
asteroid.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
=Labat FM.book Page 224 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


224

À présent, vous devez adapter la classe Player du début de ce chapitre à l’utilisation du


moteur physique. Celui-ci ne traitera pas directement une texture ni un sprite, il travaillera
avec des objets de type Body (des corps). Vous devez donc créer un corps pour chaque
élément qui devra être traité par FarseerPhysics. La classe Body se situe dans l’espace de
noms FarseerGames.FarseerPhysics.Dynamics.
La création d’un corps se fait en utilisant la classe BodyFactory (espace de noms FarseerGames
.FarseerPhysics.Factories). La méthode à utiliser dépend de la forme que vous désirez
donner à votre corps. Dans le cas du vaisseau du joueur, utilisez une forme carrée grâce à
la méthode CreateRectangleBody().

Tableau 10-1 Paramètres de la méthode CreateRectangleBody

Paramètre Description
PhysicsSimulator physicsSimulator Ce paramètre est optionnel. Si vous l’utilisez, le corps sera
automatiquement ajouté au simulateur physique.
Float width Largeur du corps.

Float height Hauteur du corps.

Float mass Masse du corps.

Un corps possède ses propres propriétés position et rotation. Le sprite est la représentation
graphique du vaisseau et le corps, sa représentation physique. Pour ne pas avoir de décalage
entre les deux, vous mettez à jour la position et l’angle de rotation du sprite en fonction
des propriétés du corps.
En ce qui concerne la rotation du corps, le point d’origine est pris au milieu du corps,
pensez donc à faire de même en ce qui concerne le sprite.
Vous pouvez appliquer une force sur un corps simplement grâce à la méthode ApplyForce
(). Elle attend comme paramètre un objet Vector2 qui contient les valeurs à appliquer sur
les axes X et Y. Pour appliquer une force sur le vaisseau et que celui-ci se dirige dans la
bonne direction, utilisez les fonctions mathématiques Sin() et Cos() à partir de son angle
de rotation en radian (rappelez-vous du cercle trigonométrique !).
Les quatre derniers tests de la méthode Update() permettent au vaisseau de disparaître
d’un côté de l’écran pour réapparaître de l’autre.
Le vaisseau du joueur utilisant le moteur physique
class Player : Sprite
{
Body body;

public Player(PhysicsSimulator physicsSimulator)


: base(Vector2.Zero)
{
body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1);
}
=Labat FM.book Page 225 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
225

public void Update(GameTime gameTime)


{
Position = body.Position;
Rotation = body.Rotation;

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Left))
body.Rotation -= 0.05f;

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Right))
body.Rotation += 0.05f;

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Up))
body.ApplyForce(new Vector2((float)Math.Sin(body.Rotation) * 100,
➥ (float)Math.Cos(body.Rotation) * -100));

if (ServiceHelper.Get<IKeyboardService>().IsKeyDown(Keys.Down))
body.ApplyForce(new Vector2((float)Math.Sin(body.Rotation) * -100,
➥ (float)Math.Cos(body.Rotation) * 100));

if (body.Position.X > ChapitreDix_2.SCREEN_WIDTH + (2 * Texture.Width))


body.Position = new Vector2(-Texture.Width, body.Position.Y);

if (body.Position.X < 0 - (2 * Texture.Width))


body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH + Texture.Width,
➥ body.Position.Y);

if (body.Position.Y > ChapitreDix_2.SCREEN_HEIGHT + (2 * Texture.Height))


body.Position = new Vector2(body.Position.X, -Texture.Height);

if (body.Position.Y < 0 - (2 * Texture.Height))


body.Position = new Vector2(body.Position.X, ChapitreDix_2.SCREEN_HEIGHT
➥ + Texture.Height);
}

public void ResetPosition()


{
body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH / 2 - (Texture.Width
➥ / 2), 9 * (ChapitreDix_2.SCREEN_HEIGHT / 10) - Texture.Height);
Position = body.Position;
body.Rotation = 0;
Rotation = body.Rotation;
Origin = new Vector2(Texture.Width / 2, Texture.Height / 2);
}
}
En ce qui concerne la classe Asteroid, il faut appliquer deux forces. La première utilise la
méthode ApplyForce() que nous venons de voir (la direction sera aléatoire). La seconde
utilise la méthode ApplyForceAtLocalPoint() qui, comme son nom l’indique, vous permet
d’appliquer une force à un point précis de votre corps. Ce point, déterminé par un objet
=Labat FM.book Page 226 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


226

de type Vector2 est donc le deuxième paramètre attendu par la méthode. Dans le cas
présent, il est choisi aléatoirement de manière à ce que la rotation ainsi subie par le corps
ne soit pas la même pour tous les astéroïdes.
Cette fois encore, n’oubliez pas de toujours coordonner position et rotation entre le sprite
et le corps !
Les astéroïdes utilisent eux aussi le moteur physique
class Asteroid : Sprite
{
static Random random = new Random(DateTime.Now.Millisecond);
Body body;
public Asteroid(PhysicsSimulator physicsSimulator, ContentManager content)
: base(Vector2.Zero)
{
base.LoadContent(content, "asteroid2");
body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1);
Origin = new Vector2(Texture.Width / 2, Texture.Height / 2);
Initialize();
}
private void Initialize()
{
body.Position = new Vector2(random.Next(0, ChapitreDix_2.SCREEN_WIDTH -
➥ Texture.Width), -Texture.Height);
body.ApplyForce(new Vector2(random.Next(-3, 3) * 1000, random.Next(-3, 3) *
➥ 1000));
body.ApplyForceAtLocalPoint(new Vector2(random.Next(-3, 3) * 100,
➥ random.Next(-3, 3) * 100), new Vector2(random.Next(0, Texture.Width),
➥ random.Next(0, Texture.Height)));
Position = body.Position;
Rotation = body.Rotation;
}
public void Update(GameTime gameTime)
{
Position = body.Position;
Rotation = body.Rotation;
if (body.Position.X > ChapitreDix_2.SCREEN_WIDTH + (2 * Texture.Width))
body.Position = new Vector2(-Texture.Width, body.Position.Y);
if (body.Position.X < 0 - (2 * Texture.Width))
body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH + Texture.Width,
➥ body.Position.Y);
if (body.Position.Y > ChapitreDix_2.SCREEN_HEIGHT + (2 * Texture.Height))
body.Position = new Vector2(body.Position.X, -Texture.Height);
if (body.Position.Y < 0 - (2 * Texture.Height))
=Labat FM.book Page 227 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
227

body.Position = new Vector2(body.Position.X, ChapitreDix_2.SCREEN_HEIGHT


➥ + Texture.Height);
}
}
Maintenant, testez le jeu. L’inertie du vaisseau et les astéroïdes qui dérivent sont vraiment
bien rendus. Au passage, vous n’avez qu’à améliorer l’image du vaisseau et celle des
astéroïdes pour avoir un rendu plus old school (figure 10-13).

Figure 10-13
La nouvelle version du
jeu… sans les collisions !

Les collisions avec FarseerPhysics


Cette nouvelle version du jeu doit faire face à un nouveau problème de taille : il n’y a
plus de gestion des collisions ! Ce n’est pas grave, vous allez maintenant apprendre à les
gérer avec le moteur physique.

Les collisions entre astéroïdes


Pour gérer les collisions, FarseerPhysics utilise encore un autre type d’objet : les formes
géométriques, Geom. Ces objets sont créés grâce à la classe GeomFactory et la méthode
correspondant à la forme que vous voulez créer. Dans le cas des astéroïdes, de forme
carrée, vous utiliserez la méthode CreateRectangleGeom ().
=Labat FM.book Page 228 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


228

Tableau 10-2 Paramètres de la méthode CreateRectangleGeom

Paramètre Description
PhysicsSimulator physicsSimulator Ce paramètre est optionnel. Si vous l’utilisez, la forme géométrique
sera automatiquement ajoutée au simulateur physique.
Body body Corps concerné.

Float width Largeur de la forme.

Float height Hauteur de la forme.

Pour