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 activer les collisions, définissez la propriété CollisionResponseEnabled de la forme


géométrique à true. Vous pouvez indiquer le coefficient de réponse au choc (la force qui
sera appliquée au corps qui entre en collision) grâce à la propriété ResitutionCoefficient.
La portion de code ci-dessous présente le constructeur de la classe Asteroid qui crée
maintenant une forme géométrique et active les collisions.
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);
GeomFactory.Instance.CreateRectangleGeom(physicsSimulator, body, 32,
➥ 32).CollisionResponseEnabled = true;
Initialize();
}
Vous pouvez relancer le jeu : à présent, les astéroïdes dérivent et entrent doucement en
collision.

Les collisions avec le joueur


Les choses vont être un peu plus complexes pour la classe Player. En effet, dès que le
vaisseau du joueur entrera en collision avec un autre élément, vous ne devrez pas le faire
rebondir, mais lui faire recommencer le jeu à zéro. Pour cela, la classe Geom met à votre
disposition l’événement OnCollision.
Un événement se produit quand quelque chose se passe dans un programme : le clic d’un
utilisateur sur un bouton, une collision entre deux entités, etc. Un événement doit être
traité avec une fonction particulière appelée event handler, qui doit avoir une signature
bien précise. Vous reliez l’événement à cette fonction en utilisant un délégué (vous les
avez déjà rencontrés lors de l’étude des méthodes asynchrones).
L’abonnement à l’événement se fait donc de la manière suivante :
public Player(PhysicsSimulator physicsSimulator)
: base(Vector2.Zero)
{
body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1);
=Labat FM.book Page 229 Vendredi, 19. juin 2009 4:01 16

Collisions et physique : créer un simulateur de vaisseau spatial


CHAPITRE 10
229

GeomFactory.Instance.CreateRectangleGeom(physicsSimulator, body, 32,


➥ 32).OnCollision += new Geom.CollisionEventHandler(CollisionOccurs);
}
Il ne vous reste plus qu’à ajouter la fonction CollisionOccurs(), qui est appelée dès qu’un
événement a lieu. Nous avons vu sa signature avec IntelliSense au moment de la création
du délégué (figure 10-14).

Figure 10-14
Signature de la fonction à créer

private bool CollisionOccurs(Geom geomA, Geom geomB, ContactList contactList)


{
return true;
}
La fonction est encore incomplète, le temps de réfléchir un peu aux traitements à effectuer…
Si le vaisseau entre en collision avec un astéroïde, il a perdu : il faut le remettre à sa position
de départ et supprimer tous les astéroïdes. Il n’y a aucun problème pour appeler la fonction
ResetPosition() de la classe Player. Cependant, vous n’avez pas accès à la liste des
astéroïdes. Vous allez donc devoir vous-même faire parvenir un événement à la classe
ChapitreDix_2 pour qu’elle s’occupe de vider la liste.
Commencez par écrire le délégué et créer l’événement.
public delegate void ShipHasExplodedEventHandler();
public event ShipHasExplodedEventHandler ShipHasExploded;
Puis, appelez l’événement dans la fonction CollisionOccurs().
private bool CollisionOccurs(Geom geomA, Geom geomB, ContactList contactList)
{
ResetPosition();
ShipHasExploded();
return true;
}
Ensuite, dans la classe ChapitreDix_2, abonnez-vous à l’événement et écrivez la fonction
qui videra la liste.
protected override void Initialize()
{
physicsSimulator = new PhysicsSimulator();
ship = new Player(physicsSimulator);
ship.ShipDestroyed += new Player.ShipDestroyedEventHandler(ship_
➥ ShipHasExploded);
base.Initialize();
}
=Labat FM.book Page 230 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


230

void ship_ShipHasExploded()
{
asteroids.Clear();
}
Vous pouvez maintenant tester le jeu et vous amuser avec !

En résumé
Dans ce chapitre, vous avez découvert :
• comment gérer les collisions en 2D avec des rectangles ;
• comment gérer les collisions en 2D au niveau des pixels ;
• ce qu’est un moteur physique, et une liste de moteurs utilisables en C# ;
• comment utiliser le moteur FarseerPhysics.
=Labat FM.book Page 231 Vendredi, 19. juin 2009 4:01 16

11
Le mode multijoueur

Pong ou Tennis for Two, les premiers jeux vidéo, étaient déjà des jeux multijoueurs.
Ainsi, le mode multijoueur, qu’il s’agisse de jouer en coopération ou de s’affronter sur un
seul et même écran, existe depuis le début des jeux vidéo, et est bien entendu toujours
utilisé de nos jours.
Avec le temps, les jeux multijoueurs ont évolué et se sont mis à utiliser les communications
réseau, reliant des stations de jeux plus ou moins éloignées (sur le même réseau local ou
via Internet).
Dans ce chapitre, nous allons développer un jeu qui utilise le scrolling et qui sera d’abord
uniquement jouable en solo, puis jouable à deux sur le même écran, et enfin à deux via le
réseau.

Jouer à plusieurs sur le même écran


Il existe deux types de jeux vidéo qui offrent une expérience multijoueur sur le même écran :
ceux où les joueurs apparaissent les uns à côté des autres dans la même vue et ceux où
l’écran est partagé en plusieurs parties.
Dans cette première partie, nous allons voir comment créer un jeu multijoueur utilisant le
principe du partage d’écran.
Un jeu en écran partagé, aussi appelé jeu en écran « splitté » (de l’anglais to split, qui
signifie séparer), est un jeu où l’écran est divisé en plusieurs zones, généralement de
même taille, chacune étant réservée à un joueur.
=Labat FM.book Page 232 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


232

Avec XNA, la division de l’écran en plusieurs parties se fera grâce à des objets de type
Viewport. Il est possible de fixer la taille de ces objets, ainsi que leur point d’origine,
chacun d’entre eux disposera de son propre repère cartésien.

Du mode solo au multijoueur : la gestion des caméras


Au chapitre 3, nous avons vu ce que sont les jeux dits tile-based et nous les avons
employés pour l’application de l’algorithme A* au chapitre 9. Dans ce chapitre, nous
irons encore plus loin avec ce type de jeu et nous allons créer un système de caméra, le
but étant de ne pas afficher toute la carte à l’écran, mais d’obtenir un effet de scrolling (en
français, défilement). Nous avons déjà abordé le scrolling au chapitre 6 : il s’agit de faire
défiler l’écran sur une carte en fonction des déplacements du joueur.

Créer un jeu solo avec effet de scrolling


Commencez par créer un nouveau projet et récupérez, des précédents projets, les fichiers
IKeyboardService.cs, KeyboardService.cs, ServiceHelper.cs, Map.cs, Sprite.cs et Tile.cs.
Tout d’abord, modifiez l’interface IKeyboardService et ajoutez-lui la signature d’une
méthode KeyHasBeenPressed. Le but de cette méthode est de détecter si le joueur a pressé
une touche, puis l’a relâchée. Ainsi, le personnage ne se déplacera d’une case qu’à
chaque fois que le joueur pressera une touche. Modifiez ensuite la classe KeyboardService
de manière à ce qu’elle prenne en compte cette nouvelle méthode.
interface IKeyboardService
{
bool IsKeyDown(Keys key);
bool KeyHasBeenPressed(Keys key);
}
Il n’y a aucune difficulté ici, d’autant plus que vous avez déjà fait la même chose dans un
précédent chapitre pour le bouton gauche de la souris.
class KeyboardService : GameComponent, IKeyboardService
{
KeyboardState lastKBState;
KeyboardState KBState;

public KeyboardService(Game game)


: base(game)
{
ServiceHelper.Add<IKeyboardService>(this);
}

public bool IsKeyDown(Keys key)


{
return KBState.IsKeyDown(key);
}
=Labat FM.book Page 233 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
233

public bool KeyHasBeenPressed(Keys key)


{
return lastKBState.IsKeyDown(key) && KBState.IsKeyUp(key);
}

public override void Update(GameTime gameTime)


{
lastKBState = KBState;
KBState = Keyboard.GetState();
base.Update(gameTime);
}
}
Intéressez-vous maintenant au système de caméra. La figure 11-1 représente votre objectif.
Vous avez une carte (celle représentée ici est tirée du chapitre 9), et le carré aux bords
noirs autour du joueur représente son champ de vision, c’est-à-dire la caméra.

Figure 11-1
Une vision partielle d’une
carte grâce à une caméra

Sur cet exemple, le joueur voit l’environnement qui l’entoure sur une distance de deux
cases, la caméra est donc un carré de 5 ¥ 5 cases. La position de la caméra sur la carte
sera déterminée par un couple de coordonnées (x, y). Vous pouvez donc utiliser un objet
de type Rectangle qui dispose de toutes ces caractéristiques.
Le sprite représentant le joueur doit toujours être placé au milieu de l’écran. Lorsque le
joueur appuie sur les touches de déplacement, ce n’est pas la position à l’écran du sprite
qui est modifiée, mais sa position sur la carte et les coordonnées de l’origine de la
caméra.
Modifiez maintenant la méthode Draw() de la classe Map afin qu’elle ne dessine que les
cases qui sont dans le champ de vision de la caméra. Parcourez le tableau tileList en
bouclant sur les dimensions de l’objet Rectangle, puis positionnez les sprites à l’écran en
fonction de leur position dans le rectangle de la caméra et appelez leur méthode Draw().
=Labat FM.book Page 234 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


234

La classe Map modifiée


class Map
{
Tile[,] tileList;
public Tile[,] TileList
{
get { return tileList; }
set { tileList = value; }
}

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, Rectangle camera)


{
for (int y = camera.Y; y < camera.Y + camera.Height; y++)
{
for(int x = camera.X; x < camera.X + camera.Width; x++)
{
tileList[y, x].Position = new Vector2((x - camera.X) * 32, (y -
➥ camera.Y) * 32);
tileList[y, x].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;
}
}
=Labat FM.book Page 235 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
235

Passez à présent à la classe principale du projet où vous utiliserez ces nouvelles fonctions.
Déclarez un objet Map, un objet Tile qui représentera le héros et enfin un objet Rectangle
pour la caméra.
Attention avec le tableau de byte lorsque vous instanciez l’objet Map. En effet, pour éviter
qu’une exception soit levée lorsque le joueur est proche des bords de la carte (puisque
la méthode Draw() de l’objet Map essaiera d’accéder à des index qui n’existent pas dans le
tableau), placez des murs sur les bords de la carte. N’oubliez pas non plus de placer le
sprite représentant le joueur au milieu de l’écran.
Lorsque vous captez une entrée utilisateur, vérifiez si la case vers laquelle il souhaite se
déplacer est un mur ou non. Si c’en est un, le déplacement ne doit pas avoir lieu.
Enfin, dans la méthode Draw(), pensez à dessiner le joueur après avoir dessiné la carte.
La classe principale du mini-jeu
public class Chapitre11 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Map map;
Tile herosA;
Rectangle cameraA;
public Chapitre11()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferWidth = 160;
graphics.PreferredBackBufferHeight = 160;
}
protected override void Initialize()
{
map = new Map(new byte[,] {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0, 1, 0},
{0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 1, 1, 0, 1, 0},
{0, 1, 3, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0},
{0, 1, 3, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 0, 3, 3, 3, 3, 3, 0, 0, 0, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 2, 2, 2, 0, 1, 0},
{0, 1, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 2, 2, 0, 1, 0},
{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
});
=Labat FM.book Page 236 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


236

cameraA = new Rectangle(0, 0, 5, 5);


herosA = new Tile(2, 2, 4);
herosA.Position = new Vector2(64, 64);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
foreach (Tile tile in map.TileList)
{
tile.LoadContent(Content, "tile");
}
herosA.LoadContent(Content, "tile");
}
protected override void Update(GameTime gameTime)
{
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Up))
{
if (map.TileList[herosA.Y - 1, herosA.X].Type >= 0)
{
cameraA.Y -= 1;
herosA.Y -= 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Right))
{
if (map.TileList[herosA.Y, herosA.X + 1].Type >= 0)
{
cameraA.X += 1;
herosA.X += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Down))
{
if (map.TileList[herosA.Y + 1, herosA.X].Type >= 0)
{
cameraA.Y += 1;
herosA.Y += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Left))
{
if (map.TileList[herosA.Y, herosA.X - 1].Type >= 0)
{
cameraA.X -= 1;
herosA.X -= 1;
}
}
base.Update(gameTime);
}
=Labat FM.book Page 237 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
237

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
map.Draw(spriteBatch, cameraA);
herosA.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
Avant de compiler le projet, ajoutez le sprite qui sera utilisé pour chacune des cases (ici
une image blanche de 32 ¥ 32 pixels). Vous pouvez ensuite l’essayer et vous amuser avec
le scrolling.
Figure 11-2
Votre jeu en mode solo

Adapter les caméras au multijoueur


À présent, nous allons modifier le jeu de manière à ce qu’il puisse accueillir deux joueurs
en simultané sur le même écran. Dans le code source de cet ouvrage, le projet correspondant
est nommé Chapitre 11_2.
La première chose à faire est de vous occuper des objets Viewport. Créez-en deux, un
pour chaque joueur. Ici, ils seront appelés viewportA pour la portion supérieure de l’écran
et viewportB pour la portion inférieure de l’écran.
Spécifiez les dimensions des deux vues en définissant leurs propriétés Width et Height.
Dans le programme exemple lié à ce chapitre, la largeur d’une vue est la même que la
largeur de la fenêtre de jeu, et sa hauteur correspond à la moitié de celle de la fenêtre de
jeu. Pensez donc à augmenter la hauteur de la fenêtre de jeu si vous voulez que les deux
vues soient des carrés.
Enfin, décalez la deuxième vue en hauteur grâce à sa propriété Y. Les deux vues seront
donc alignées verticalement ; pour les aligner horizontalement, vous modifierez simplement
la propriété X.
viewportA.Width = graphics.PreferredBackBufferWidth;
viewportA.Height = graphics.PreferredBackBufferHeight / 2;
=Labat FM.book Page 238 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


238

viewportB.Y = graphics.PreferredBackBufferHeight / 2;
viewportB.Width = graphics.PreferredBackBufferWidth;
viewportB.Height = graphics.PreferredBackBufferHeight / 2;
Déclarez un nouvel objet Tile qui représentera le deuxième joueur, ainsi qu’un nouvel
objet Rectangle pour la caméra du deuxième joueur et faites-les bouger lorsque des
touches du clavier seront utilisées (ici les touches Z-Q-S-D).

Xbox 360
Dans l’exemple de jeu qui vous est proposé ici, les deux joueurs utilisent le clavier. Vous pouvez modifier
le projet pour qu’il soit utilisable avec les manettes de la Xbox 360, vous différencierez les entrées utilisateur
grâce à l’énumération PlayerIndex.
Sur Xbox 360, les manettes peuvent être complétées par des claviers. L’état de ces claviers se récupère
de la manière suivante :
Keyboard.GetState(PlayerIndex.Two);

Enfin, vous n’avez plus qu’à dessiner dans les deux vues. Pour vous placer sur une vue,
modifiez la propriété Viewport de l’objet GraphicsDevice. Le dessin des sprites se fait de
manière classique.
La classe principale du jeu multijoueur
public class Chapitre11_2 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Map map;
Tile herosA;
Tile herosB;
Rectangle cameraA;
Rectangle cameraB;
Viewport viewportA;
Viewport viewportB;
public Chapitre11_2()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
graphics.PreferredBackBufferWidth = 160;
graphics.PreferredBackBufferHeight = 320;
}
protected override void Initialize()
{
=Labat FM.book Page 239 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
239

map = new Map(new byte[,] {


{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0, 1, 0},
{0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 1, 1, 0, 1, 0},
{0, 1, 3, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0},
{0, 1, 3, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 0, 3, 3, 3, 3, 3, 0, 0, 0, 2, 2, 0, 0, 1, 0},
{0, 1, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 2, 2, 2, 0, 1, 0},
{0, 1, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 2, 2, 0, 1, 0},
{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
});
cameraA = new Rectangle(0, 0, 5, 5);
cameraB = new Rectangle(1, 1, 5, 5);
herosA = new Tile(cameraA.X + 2, cameraA.Y + 2, 4);
herosA.Position = new Vector2(64, 64);
herosB = new Tile(cameraB.X + 2, cameraB.Y + 2, 4);
herosB.Position = new Vector2(64, 64);
viewportA.Width = graphics.PreferredBackBufferWidth;
viewportA.Height = graphics.PreferredBackBufferHeight / 2;
viewportB.Y = graphics.PreferredBackBufferHeight / 2;
viewportB.Width = graphics.PreferredBackBufferWidth;
viewportB.Height = graphics.PreferredBackBufferHeight / 2;
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
foreach (Tile tile in map.TileList)
{
tile.LoadContent(Content, "tile");
}
herosA.LoadContent(Content, "tile");
herosB.LoadContent(Content, "tile");
}
protected override void Update(GameTime gameTime)
{
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Up))
{
if (map.TileList[herosA.Y - 1, herosA.X].Type >= 0)
{
cameraA.Y -= 1;
=Labat FM.book Page 240 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


240

herosA.Y -= 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Right))
{
if (map.TileList[herosA.Y, herosA.X + 1].Type >= 0)
{
cameraA.X += 1;
herosA.X += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Down))
{
if (map.TileList[herosA.Y + 1, herosA.X].Type >= 0)
{
cameraA.Y += 1;
herosA.Y += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Left))
{
if (map.TileList[herosA.Y, herosA.X - 1].Type >= 0)
{
cameraA.X -= 1;
herosA.X -= 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Z))
{
if (map.TileList[herosB.Y - 1, herosB.X].Type >= 0)
{
cameraB.Y -= 1;
herosB.Y -= 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.D))
{
if (map.TileList[herosB.Y, herosB.X + 1].Type >= 0)
{
cameraB.X += 1;
herosB.X += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.S))
{
if (map.TileList[herosB.Y + 1, herosB.X].Type >= 0)
{
cameraB.Y += 1;
herosB.Y += 1;
}
}
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.Q))
{
=Labat FM.book Page 241 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
241

if (map.TileList[herosB.Y, herosB.X - 1].Type >= 0)


{
cameraB.X -= 1;
herosB.X -= 1;
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
GraphicsDevice.Viewport = viewportA;
spriteBatch.Begin();
map.Draw(spriteBatch, cameraA);
herosA.Draw(spriteBatch);
spriteBatch.End();
GraphicsDevice.Viewport = viewportB;
spriteBatch.Begin();
map.Draw(spriteBatch, cameraB);
herosB.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}

Avancé
Rien ne vous empêche d’utiliser le même objet SpriteBatch pour dessiner dans différents Viewport.
Cependant, vous devrez faire un appel à Begin() et à End() pour chacun des objets Viewport !

Vous pouvez à présent essayer le jeu. Les deux joueurs ont chacun une portion d’écran qui
leur est réservée : ils peuvent se déplacer sans problèmes, mais sans se voir (figure 11-3).
Figure 11-3
Les deux joueurs ne se voient pas
=Labat FM.book Page 242 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


242

Il vous faut donc créer deux nouveaux objets Tile qui correspondront aux représentations
d’un joueur sur la vue de l’autre et inversement.
Pour commencer, créez un nouveau type de Tile pour que les deux joueurs n’apparais-
sent pas de la même couleur sur la même vue. Modifiez simplement le constructeur de la
classe et ajoutez une nouvelle branche au switch.
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;
case 4:
Color = Color.Black;
this.type = TileType.Human;
break;
case 5:
Color = Color.Red;
this.type = TileType.Human;
break;
default:
this.type = TileType.Normal;
break;
}
}
Dans une vue, le personnage que le joueur ne contrôle pas apparaîtra donc en rouge.
Lors du dessin de la vue du joueur A, vérifiez que le joueur B est dans le champ de la
caméra A. Si c’est le cas, modifiez la position à l’écran de sa représentation et dessinez-la.
Faites le même traitement pour le joueur A sur la vue du joueur B.
Les ajouts à effectuer dans la classe principale
Tile herosBOnA;
Tile herosAOnB;
=Labat FM.book Page 243 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
243

protected override void Initialize()


{
// …
herosBOnA = new Tile(0, 0, 5);
herosAOnB = new Tile(0, 0, 5);
}

protected override void LoadContent()


{
// …
herosAOnB.LoadContent(Content, "tile");
herosBOnA.LoadContent(Content, "tile");
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.Black);

GraphicsDevice.Viewport = viewportA;
spriteBatch.Begin();
map.Draw(spriteBatch, cameraA);
herosA.Draw(spriteBatch);
if (cameraA.Contains(herosB.X, herosB.Y))
{
herosBOnA.Position = new Vector2((herosB.X - cameraA.X) * 32, (herosB.Y -
➥ cameraA.Y) * 32);
herosBOnA.Draw(spriteBatch);
}
spriteBatch.End();

GraphicsDevice.Viewport = viewportB;
spriteBatch.Begin();
map.Draw(spriteBatch, cameraB);
herosB.Draw(spriteBatch);
if (cameraB.Contains(herosA.X, herosA.Y))
{
herosAOnB.Position = new Vector2((herosA.X - cameraB.X) * 32, (herosA.Y -
➥ cameraB.Y) * 32);
herosAOnB.Draw(spriteBatch);
}
spriteBatch.End();

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

Développement XNA pour la XBox et le PC


244

À présent, vous pouvez tester le jeu, il fonctionne à merveille (figure 11-4).

Figure 11-4
Cette fois-ci, les deux joueurs peuvent
se croiser sans problèmes

Personnaliser les différentes vues


Maintenant que vous connaissez le B.A-BA des vues avec XNA, voyons quelques précisions
sur leur fonctionnement. Nous laisserons l’exemple de jeu précédent de côté.
Pour nettoyer le contenu d’une vue et lui appliquer une couleur, sélectionnez la vue
concernée puis, comme vous le faites d’ordinaire pour la totalité de l’écran, appelez la
méthode Clear() de l’objet GraphicsDevice et passez-lui une couleur.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
GraphicsDevice.Viewport = viewA;
GraphicsDevice.Clear(Color.Yellow);
base.Draw(gameTime);
}
Vos vues ne doivent pas forcément remplir toute la surface de l’écran. Vous pouvez les placer
où vous le voulez et leur donner des dimensions farfelues si vous le désirez (figure 11-5).
Différentes tailles de vues
public class Chapitre11_3 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport viewA;
Viewport viewB;
=Labat FM.book Page 245 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
245

Figure 11-5
Les vues se placent où vous le voulez

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

protected override void Initialize()


{
viewA.Width = 100;
viewA.Height = 100;
viewA.X = 50;

viewB.Width = 300;
viewB.Height = 100;
viewB.X = 250;
viewB.Y = 100;

base.Initialize();
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

GraphicsDevice.Viewport = viewA;
GraphicsDevice.Clear(Color.Yellow);

GraphicsDevice.Viewport = viewB;
GraphicsDevice.Clear(Color.Green);

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

Développement XNA pour la XBox et le PC


246

Même si vous utilisez une vue, rien ne vous empêche de dessiner d’une manière plus
classique sur la totalité de la fenêtre (figure 11-6).
public class Chapitre11_3 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport view;

Sprite sprite;
Sprite sprite2;

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

protected override void Initialize()


{
view.Width = 300;
view.Height = 100;
view.X = 250;
view.Y = 100;

sprite = new Sprite(Vector2.Zero);


sprite2 = new Sprite(Vector2.Zero);

base.Initialize();
}

protected override void LoadContent()


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

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
sprite2.Draw(spriteBatch);
spriteBatch.End();

GraphicsDevice.Viewport = view;
GraphicsDevice.Clear(Color.Green);
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();

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

Le mode multijoueur
CHAPITRE 11
247

Figure 11-6
Affichage classique dans
la fenêtre tout en utilisant
une vue

Toutefois, faites attention à l’ordre des dessins. Si vous voulez dessiner dans la fenêtre
principale après avoir utilisé une vue particulière, le code suivant ne fonctionnera pas
(figure 11-7).

Figure 11-7
De cette manière, vous
ne pouvez pas redessiner
dans la vue principale

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

GraphicsDevice.Viewport = view;
GraphicsDevice.Clear(Color.Green);
spriteBatch.Begin();
sprite.Draw(spriteBatch);
=Labat FM.book Page 248 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


248

spriteBatch.End();

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

base.Draw(gameTime);
}
Vous devrez enregistrer la vue principale pour pouvoir la réutiliser plus loin.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
Viewport mainView = GraphicsDevice.Viewport;

GraphicsDevice.Viewport = view;
GraphicsDevice.Clear(Color.Green);
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();

GraphicsDevice.Viewport = mainView;
spriteBatch.Begin();
sprite2.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
Si vous désirez faire jouer plus de quatre joueurs dans la même partie ou si vous désirez
connecter des joueurs à travers le réseau, vous ne pourrez pas vous contenter du jeu en
écran séparé.

Le multijoueur en réseau
Dans cette deuxième partie, nous allons nous occuper des mécanismes qui vous permettront
de réaliser un jeu multijoueur en réseau. Le but est ici de connecter de nombreux joueurs
situés à des emplacements géographiques différents. Cependant, vous devrez faire face à
de nouvelles contraintes, telles que la gestion des sessions ou encore l’envoi d’informations
via le réseau.

S’appuyer sur la plate-forme Live


Windows Live est le nom de la plate-forme de services Internet de Microsoft. Le point fort
de ces services est qu’ils utilisent tous un système d’authentification unifié : les comptes
Live.
=Labat FM.book Page 249 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
249

La gestion du réseau avec XNA est basée sur ce système de comptes Live. On distingue
deux types de comptes :
• Les comptes Live, enregistrés sur les serveurs de Microsoft et qui sont liés à un gamer tag.
Ce type de compte est nécessaire pour créer un jeu multijoueur et pour pouvoir y jouer.
• Les comptes locaux, qui, comme leur nom l’indique, ne peuvent être utilisés que
localement et qui n’offrent qu’un accès restreint aux services de la plate-forme Live.
Pour utiliser le réseau avec XNA, vous passerez par les gamer services. Pour mémoire,
ils s’initialisent de la manière suivante :
Components.Add(new GamerServicesComponent(this));

En phase de test
Lorsque vous testerez les programmes qui utilisent le réseau avec XNA, vous devrez utiliser plusieurs
machines (PC, Xbox ou Zune). En effet, il est impossible de lancer plusieurs instances d’une application
qui utilise les gamer services sur la même machine.

Comme nous avons déjà vu au chapitre 8 comment ouvrir les différents menus de ces
services, il n’en sera donc pas question ici.

Implémenter les fonctionnalités de jeu en réseau


Une partie en réseau, ou session, se gère via un objet de type NetworkSession.

Les sessions et la connexion à la plate-forme de jeu en réseau


La première méthode de cette classe qu’il vous faut utiliser est la méthode Create(). C’est
elle qui permet de créer une partie en réseau. Le tableau 11-1 répertorie les paramètres
les plus courants pour cette méthode :

Tableau 11-1 Paramètres du premier constructeur de Create()

Paramètre Description
NetworkSessionType sessionType Type de la session à créer (réseau local, inter ne à la machine, classement
sur Internet, etc.).
Int maxLocalGamers Nombre maximal de comptes locaux autorisés dans la partie.

Int maxGamers Nombre de joueurs maximal.

Une fois la session créée, appelez sa méthode Update() à chaque frame. Cette méthode
réalise les actions suivantes :
• envoyer les paquets réseaux ;
• mettre à jour l’état de la session ;
=Labat FM.book Page 250 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


250

• récupérer les paquets réseau entrants.


L’appel de cette méthode vous épargne les soucis de threads et de synchronisation que
vous auriez pu rencontrer en programmant vous-même la gestion du réseau.
Dans l’exemple suivant, nous allons créer une partie sur le réseau local lorsque le joueur
appuiera sur la touche C de son clavier. Notez que vous pouvez récupérer des informations
sur la partie grâce aux propriétés de l’objet NetworkSession. Dans le code ci-dessous, une
vérification de la propriété IsHost permet de déterminer si l’instance du programme
héberge la partie ou non.
Exemple de création d’une partie sur le réseau local
public class Chapitre11_Server : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
NetworkSession session;
public Chapitre11_Server()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
ServiceHelper.Game = this;
Components.Add(new KeyboardService(this));
Components.Add(new GamerServicesComponent(this));
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void Update(GameTime gameTime)
{
if (ServiceHelper.Get<IKeyboardService>().KeyHasBeenPressed(Keys.C) &&
➥ session == null)
session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31);
if (session != null)
{
if (session.IsHost)
Window.Title = "Je suis le serveur (" + session.SessionState
➥ .ToString() + ")";
session.Update();
}
base.Update(gameTime);
}
=Labat FM.book Page 251 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
251

protected override void Draw(GameTime gameTime)


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

Identifier les parties en cours disponibles


Pour rejoindre une partie, vous devez d’abord rechercher une liste de parties disponibles.
Pour cela, vous disposez de la méthode statique Find() de la classe NetworkSession, ou
bien, si vous souhaitez utiliser des processus asynchrones, des méthodes BeginFind() et
EndFind(). Comme nous l’avons vu au chapitre 8, la méthode synchrone bloque le jeu,
alors que la méthode asynchrone effectue le traitement en arrière-plan. Là encore, vous
devez spécifier le type de partie réseau. Vous pouvez ajouter un paramètre NetworkSession
Properties, qui filtre la liste de parties disponibles.
Dans les deux cas, vous récupérerez une collection de AvailableNetworkSession de type
AvailableNetworkSessionCollection.
Pour rejoindre une partie, vous n’avez plus qu’à utiliser la méthode Join de la classe
NetworkSession.
Dans l’exemple suivant, si le joueur appuie sur la touche C (ou le bouton A de la manette
Xbox), un serveur est créé. S’il appuie sur S (ou B), le programme se recherche les
parties disponibles et, si la recherche retourne au moins un résultat, il s’y connecte.
Si le programme est client sur un serveur, il affiche le nom du serveur dans sa barre de
titre, sinon il affiche « Je suis le serveur » (figure 11-8).
Création, recherche de parties et connexion
public class Chapitre11_4 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
NetworkSession session;
AvailableNetworkSessionCollection availableSessions;
public Chapitre11_4()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
Components.Add(new GamerServicesComponent(this));
}
protected override void Initialize()
{
base.Initialize();
}
=Labat FM.book Page 252 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


252

protected override void LoadContent()


{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void Update(GameTime gameTime)
{
if ((Keyboard.GetState().IsKeyDown(Keys.C) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.A)) && session == null)
session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31);
if ((Keyboard.GetState().IsKeyDown(Keys.S) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.B)) &&
➥ SignedInGamer.SignedInGamers.Count != 0 && session == null)
{
availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink,
➥ 2, null);
if (availableSessions != null && availableSessions.Count > 0)
session = NetworkSession.Join(availableSessions[0]);
}
if (session != null)
{
if (session.IsHost)
Window.Title = "Je suis le serveur";
else
Window.Title = "Je suis client sur la partie de " +
➥ session.Host.ToString();
session.Update();
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}

Figure 11-8
Cette instance du jeu est correctement connectée à une partie en réseau
=Labat FM.book Page 253 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
253

Transmettre les données : la partie est en cours


Pour envoyer des données, vous utilisez un objet de type PacketWriter. Vous y entrez les
données à envoyer (un très grand nombre de types de données est supporté), puis passez
cet objet à la méthode SendData () du joueur qui envoie les données. Cette méthode
attend un autre paramètre correspondant au mode de transmission des données (voir le
tableau 11-2).

En pratique
Tous les joueurs (y compris l’expéditeur) reçoivent ces données.

Tableau 11-2 Méthodes d’expédition

Nom Description
SendDataOptions.Chat Les données envoyées correspondent à une conversation entre
joueurs.
SendDataOptions.InOrder Les données peuvent être perdues sur le réseau, mais lorsqu’elles
arrivent, elles sont toujours dans l’ordre où elles ont été envoyées.
SendDataOptions.None Les données peuvent être perdues et arriver dans le désordre.

SendDataOptions.Reliable Les données ne peuvent pas être perdues, mais leur ordre d’arrivée
n’est pas garanti.
SendDataOptions.ReliableInOrder Les données ne peuvent pas être perdues et arriveront dans le bon
ordre.

Bien évidemment, si vous utilisez une option qui garantit une bonne intégrité de la trans-
mission (pas de perte et arrivée des données dans le bon ordre), les performances sont
altérées.
Pour recevoir des données, recourez à un objet de type PacketReader. Lorsque des
données viennent d’arriver, la propriété IsDataAvailable du joueur local (objet de type
LocalNetworkGamer) est à true.
Vous utiliserez ensuite la méthode ReceiveData () du joueur local pour récupérer ces
données. Vous devez passer à cette méthode une référence à un objet de type NetworkGamer,
qui correspond à l’expéditeur du paquet.
Ensuite, pour extraire les données du PacketReader, pensez à la méthode ReadType (), où
Type est le type des données à extraire.
La méthode Dispose() de l’objet NetworkSession sert à quitter une partie multijoueur.
Ensuite, mettez sa référence (et éventuellement celle de la collection AvailableNetwork
SessionCollection) à null.
L’exemple de code ci-dessous est une amélioration de l’exemple précédent : il vous
permet, lorsque vous appuyez sur la touche M du clavier (ou le bouton Y de la manette),
d’envoyer l’heure courante sous forme de chaîne de caractères à tous les joueurs (sauf
=Labat FM.book Page 254 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


254

l’expéditeur) connectés au serveur. Lorsque vous appuyez sur Q (ou le bouton Back dans
le cas de la manette), il ferme la connexion à la partie.
Exemple de dialogue entre client et serveur
public class Chapitre11_4 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
NetworkSession session;
AvailableNetworkSessionCollection availableSessions;
PacketReader packetReader;
PacketWriter packetWriter;
string lastStringReceived = "";
public Chapitre11_4()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
Components.Add(new GamerServicesComponent(this));
}
protected override void Initialize()
{
base.Initialize();
packetReader = new PacketReader();
packetWriter = new PacketWriter();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("font");
}
protected override void Update(GameTime gameTime)
{
if ((Keyboard.GetState().IsKeyDown(Keys.Q) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.Back)) && session != null)
{
session.Dispose();
session = null;
availableSessions = null;
}
if ((Keyboard.GetState().IsKeyDown(Keys.C) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.A)) && session == null)
session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31);
=Labat FM.book Page 255 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
255

if ((Keyboard.GetState().IsKeyDown(Keys.S) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.B)) &&
➥ SignedInGamer.SignedInGamers.Count != 0 && session == null)
{
availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink,
➥ 2, null);
if (availableSessions != null && availableSessions.Count > 0)
session = NetworkSession.Join(availableSessions[0]);
}
if ((Keyboard.GetState().IsKeyDown(Keys.M) || GamePad.GetState
➥ (PlayerIndex.One).IsButtonDown(Buttons.Y)) && session != null)
{
packetWriter.Write(DateTime.Now.ToString());
session.LocalGamers[0].SendData(packetWriter, SendDataOptions.);
}
if (session != null)
{
session.Update();
LocalNetworkGamer gamer = session.LocalGamers[0];
if (gamer.IsDataAvailable)
{
NetworkGamer sender;
gamer.ReceiveData(packetReader, out sender);
if (gamer != sender)
lastStringReceived = packetReader.ReadString();
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
if (session != null)
{
if (session.IsHost)
spriteBatch.DrawString(font, "Serveur (" + SignedInGamer
➥ .SignedInGamers[0].Gamertag + ")", new Vector2(100, 100),
➥ Color.White);
else
spriteBatch.DrawString(font, "Client (" + SignedInGamer
➥ .SignedInGamers[0].Gamertag + ") connecte sur " +
➥ session.Host.ToString() , new Vector2(100, 100), Color.White);
=Labat FM.book Page 256 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


256

spriteBatch.DrawString(font, session.AllGamers.Count + " joueur(s)


➥ connecte(s)", new Vector2(100, 150), Color.White);
for (int i = 0; i < session.AllGamers.Count; i++)
{
spriteBatch.DrawString(font, session.AllGamers[i].Gamertag, new
➥ Vector2(100, 200 + (i * 50)), Color.White);
}

spriteBatch.DrawString(font, "Dernier message recu", new Vector2(500,


➥ 100), Color.White);
spriteBatch.DrawString(font, lastStringReceived, new Vector2(500, 150),
➥ Color.White);
session.Update();
}

spriteBatch.End();

base.Draw(gameTime);
}
}
La figure 11-9 présente l’écran du serveur (sur la Xbox 360) et sur la figure 11-10, l’écran du
client (sur l’ordinateur). Dans les deux cas, vous retrouvez la liste des joueurs présents
dans la session, ainsi que la date du dernier message reçu.

Figure 11-9
L’écran côté serveur (sur Xbox 360)

Figure 11-10
L’écran côté client
=Labat FM.book Page 257 Vendredi, 19. juin 2009 4:01 16

Le mode multijoueur
CHAPITRE 11
257

En résumé
Dans ce chapitre, vous avez découvert :
• comment créer un jeu en deux dimensions avec un effet de scrolling ;
• comment gérer des vues avec des objets de type Viewport pour créer un jeu multijoueur
sur le même écran ;
• ce qu’est la plate-forme Windows Live ;
• comment fonctionnent les communications réseau avec XNA.
À la fin de ce chapitre, vous disposez de toutes les connaissances et notions nécessaires
pour développer un jeu multijoueur en deux dimensions. Voyons à présent comment ajouter
la troisième dimension/entrer dans la troisième dimension.
=Labat FM.book Page 258 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 259 Vendredi, 19. juin 2009 4:01 16

12
Les bases de
la programmation 3D

À ce stade du livre, vous avez normalement déjà écrit plusieurs milliers de lignes de
codes, vous avez peut-être même déjà créé plusieurs jeux en 2D, vous les avez peut-être
déjà partagés ou vendus, etc. Mais vous ne savez pas encore tout de XNA ! En effet, le
framework vous permet non seulement de créer des jeux en 2D, mais il met également la
troisième dimension à portée de souris.
Ce chapitre constitue une introduction à la programmation en 3D : nous commencerons
par découvrir ensemble les bases théoriques, puis vous passerez à la pratique en dessinant
vos premières formes, en y ajoutant couleurs, textures et lumières et enfin en chargeant
un premier modèle 3D.

L’indispensable théorie
Avant de se lancer corps et âme dans la création d’une scène en 3D avec XNA, il est bon
de s’intéresser à quelques notions théoriques, faute de quoi vous risqueriez de ne pas
vraiment comprendre le code que vous écrivez.

Le système de coordonnées
Après avoir lu (et pratiqué !) tous les chapitres précédents, vous connaissez maintenant
par cœur le système de coordonnées qui est utilisé par XNA pour les scènes en deux
dimensions : l’origine est dans le coin supérieur gauche de l’écran avec les axes représentés
tels que sur la figure 12-1.
=Labat FM.book Page 260 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


260

Figure 12-1
Système de coordonnées 2D

Lorsqu’il s’agit de 3D, une nouvelle composante (Z) vient s’ajouter aux deux autres (X
et Y). Celle-ci représente la profondeur. Il existe deux types de systèmes de coordonnées
pour représenter ces trois axes : le repère main gauche et le repère main droite. La diffé-
rence entre ces deux repères est la direction de l’axe Z. Essayez : alignez vos mains sur
l’axe X, le bout de vos doigts pointant vers la droite, relevez vos doigts (sauf le pouce)
dans la direction de l’axe Y et enfin, écartez le pouce du reste des doigts, ce dernier vous
donne la direction de l’axe Z.
Concrètement (et pour vous éviter de vous casser le poignet), dites-vous que dans le
repère main gauche, les valeurs de Z augmentent lorsque vous partez de l’écran vers un
point éloigné de vous. Dans le repère main droite, c’est tout simplement l’inverse ; plus
les points sont loin de vous, plus les valeurs de Z diminuent (figure 12-2).

Figure 12-2
Repère main gauche et repère main droite

XNA utilise par défaut le repère main droite, donc plus la valeur de Z d’un objet est petite,
plus il est loin de vous.

Construire des primitives à partir de vertices


L’élément le plus simple qui compose une scène 3D est le vertex (au pluriel, vertices).
Le terme vertex est souvent traduit par sommet, or dans XNA, un vertex est bien plus
=Labat FM.book Page 261 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
261

qu’un simple point de l’espace, il contient d’autres informations telles que la couleur,
la texture, etc.
Lorsque l’on crée des objets en 3D, il faut non seulement spécifier la position des vertices
et d’éventuelles informations, mais également préciser le type de primitive à utiliser. Une
primitive définit la manière dont une collection de vertices doit être affichée. Au final, on
ne dessine donc pas directement les vertices, mais les primitives.
Les vertices peuvent être dessinés sous forme de points déconnectés les uns des autres,
sous forme de lignes ou encore sous forme de triangles. Généralement, c’est cette
dernière catégorie qui est utilisée pour dessiner n’importe quel objet. Ainsi, pour dessiner
un carré, vous aurez besoin de deux triangles (figure 12-3).

Figure 12-3
Un carré composé de triangles

Dans XNA, les différents types de primitives sont contenus dans l’énumération PrimitiveType.
Le tableau 12-1 répertorie les primitives à votre disposition.

Tableau 12-1 Primitives 3D

Nom Description Illustration


PointList Les vertices sont isolés les uns des autres. Figure 12-4
Les vertices isolés
les uns des autres

LineList Les vertices sont représentés par paire, Figure 12-5


les éléments de chaque paire étant reliés Les vertices groupés
entre eux. Attention, vous devrez passer par paire
un nombre pair de vertices, sans quoi le
dessin sera impossible.

LineStrip Tous les vertices sont représentés en une Figure 12-6


seule ligne. Cette méthode est utilisée dans Les vertices composant
le mode wire-frame (fil de fer en français), une seule et même ligne
très utile dans les applications en temps
réel, puisque facile à afficher.
=Labat FM.book Page 262 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


262

Tableau 12-1 Primitives 3D (suite)

Nom Description Illustration

TriangleList Les vertices sont représentés par groupe Figure 12-7


de trois, sous la forme de triangles isolés. Les vertices sous forme
de triangles isolés

TriangleStrip Les vertices sont représentés sous forme Figure 12-8


de triangles connectés entre eux. Il y a un Les vertices sous forme
gain de performances puisque les vertices de triangles connectés
communs à deux triangles ne sont repré-
entre eux
sentés qu’une fois.

TriangleFan Les vertices sont une nouvelle fois repré- Figure 12-9
sentés sous forme de triangles, et tous ces Les triangles ont tous
triangles ont un vertex en commun. un vertex en commun

Les vecteurs dans XNA


Dans XNA, il existe trois types de vecteurs :
• les objets Vector2, que vous avez déjà rencontrés lorsque nous avons travaillé sur des
scènes en deux dimensions ;
• les objets Vector3, qui disposent en plus de la composante Z ;
• les objets Vector4, qui disposent d’une quatrième composante, susceptible d’être utilisée
par exemple pour contenir des informations sur la couleur.
Une fois de plus, XNA nous simplifie la vie en fournissant des méthodes de calcul sur les
vecteurs (notez tout de même que le fait de comprendre les aspects mathématiques qui se
cachent derrière ces fonctions ne peut être que bénéfique). Ainsi, la structure Vector3
dispose d’une vingtaine de méthodes utilitaires. Le tableau 12-2 en présente quelques-unes.
Lorsque vous développerez vos premiers jeux en 3D, vous utiliserez la plupart de ces
méthodes.

Tableau 12-2 Principales méthodes de calcul sur les vecteurs

Méthode Description
Add Effectue la somme de deux vecteurs.

Substract Soustrait un vecteur à un autre.

Distance Calcule la distance entre deux vecteurs.


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

Les bases de la programmation 3D


CHAPITRE 12
263

Les matrices et les transformations


Les matrices constituent l’outil mathématique indispensable pour effectuer :
• des rotations, pour faire tourner un objet autour d’un ou plusieurs axes ;
• des mises à l’échelle, que cela soit pour agrandir ou diminuer la taille d’un objet ;
• des translations, c’est-à-dire déplacer un objet.
Les matrices sont des tableaux de données à deux dimensions. Dans XNA, ce tableau
contient 4 lignes et 4 colonnes, et il est représenté par la structure Matrix.
Comme toujours, XNA nous simplifie le travail. Même si vous n’avez jamais entendu parler
de matrices et que vous ignorez tout des règles de calcul qui s’appliquent, vous ne serez
pas gêné dans leur manipulation.

Conseil
Toutefois, pour comprendre parfaitement les transformations, nous vous conseillons de jeter un coup d’œil
à un cours d’algèbre linéaire. Citons par exemple :
http://www.librecours.org/cgi-bin/course?callback=info&elt=562).

Tableau 12-3 Principales méthodes de calcul sur les matrices

Méthode Description
CreateRotationX Crée une matrice de rotation pour chacun des axes.
CreateRotationY
CreateRotationZ
Translation Crée une matrice de translation.

CreateLookAt Crée une matrice utilisée pour positionner la camera en définissant son emplacement
et la position vers laquelle elle est tournée.

Gérer les effets sous XNA


La gestion des effets (qu’il s’agisse de la lumière ou de techniques de rendus plus
complexes) est un point crucial dans la réalisation d’un jeu.
Au chapitre suivant, nous verrons comment créer des fichiers dédiés aux effets en HLSL
(High Level Shading Langage). Pour l’instant, nous allons nous concentrer sur le maniement
de la classe BasicEffect qui mettra à disposition tout le nécessaire pour un premier jeu.
Le tableau 12-4 liste les propriétés qui vous serviront le plus.
=Labat FM.book Page 264 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


264

Tableau 12-4 Principales propriétés de la classe BasicEffect

Propriété Effet
LightingEnabled Si elle est définie à false, la scène possède une source de lumière qui illumine toutes les
faces de tous les objets, sinon, la source de lumière sera celle définie par l’effet.
AmbientLightColor Cette propriété permet de définir la couleur de la lumière ambiante qui illumine de la
même manière toutes les faces de tous les objets si la propriété LightEnabled est définie
à true.
DirectionalLight0 Ces propriétés définissent des lumières directionnelles qui seront utilisées seulement si
DirectionalLight1 la propriété LightingEnabled est définie à true.
DirectionalLight2
FogColor Ces propriétés font apparaître du brouillard sur les scènes. Spécifiez la couleur du
FogStart brouillard, ainsi que la distance où il commence et où il se ter mine.
FogEnd

À la manière d’un objet SpriteBatch, il faut dessiner les objets auxquels vous souhaitez
appliquer un effet entre les méthodes Begin() et End() de l’objet BasicEffect.
effect.Begin();
// Dessin
effect.End();
Un effet possède une ou plusieurs techniques. Chacune de ces techniques possède à son
tour une ou plusieurs passes. Une passe contient les différents traitements à effectuer.
Pour appliquer un effet, vous devrez donc parcourir la liste de passes d’une technique
donnée. Cependant, la classe BasicEffect n’a qu’une seule technique et une seule passe.
Nous reviendrons sur ces notions en détail au chapitre 13 dans la section « Finaliser un
effet, les techniques et les passes ».
effect.Begin();
foreach(EffectPass CurrentPass in effect.CurrentTechnique.Passes)
{
CurrentPass.Begin();
CurrentPass.End();
}
effect.End();

Comprendre la projection
Comment XNA transforme-t-il une scène en 3D vers une image en 2D que votre écran
pourra afficher ? C’est le principe de la projection, le but étant d’obtenir la même image
que ce que nos yeux verraient si nous étions dans la scène. XNA supporte deux types de
projection :
• La projection en perspective est la plus couramment utilisée. Dans ce type de projection,
l’ensemble des objets présents dans un volume en forme de pyramide tronquée (défini
par un plan proche (near plane), un plan éloigné (far plane) et un angle appelé champ
=Labat FM.book Page 265 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
265

de vision (FOV, pour field of view)) sont projetés vers le sommet de la pyramide
(figure 12-10). Ainsi, plus un objet est éloigné de la caméra, plus il semble petit, et
inversement.
• La projection orthogonale, aussi appelée projection parallèle. Dans ce type de projection,
la composante Z est ignorée, c’est-à-dire qu’un objet éloigné de la caméra semblera
aussi gros que s’il était tout proche de celle-ci.

Figure 12-10
Projection en perspective

Vous connaissez à présent tous les éléments que nous allons utiliser pour créer et dessiner
des scènes en 3D. La deuxième partie de ce chapitre met ces notions en pratique : nous
allons dessiner des formes simples puis les texturer, déplacer la caméra et un objet et
enfin charger un modèle en 3D.

Dessiner des formes


Passons à présent à la pratique. Dans un premier temps, nous verrons comment configurer
la matrice de projection et la matrice de vue, puis comment dessiner des formes dans une
scène en 3D.

La caméra et la matrice de projection


La première chose à faire avant de dessiner des formes est de créer une caméra, faute de
quoi vous ne pourriez pas voir la scène. Créez un nouveau projet et ajoutez quelques
champs :
Matrix projection;
Matrix view;
Matrix world;
Ensuite, il faut définir la matrice de projection dans la méthode Initialize(). Pour cela,
vous pouvez utiliser la méthode statique CreatePerspectiveFieldOfView (). La méthode
=Labat FM.book Page 266 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


266

attend en paramètre les différentes mesures qui constituent la pyramide tronquée de la


projection. Comme l’angle du champ de vision doit être passée en radians, utilisez la
méthode ToRadians() de la classe MathHelper, ou comme ici, une des constantes prédéfinies.
Il faut également définir le rapport (la variable aspectRatio) entre la largeur de l’écran et
sa hauteur. Enfin, le dernier paramètre attendu est une référence vers la matrice où le
résultat de cette méthode sera stocké.

En pratique
Il existe une version non statique de cette méthode. L’intérêt d’utiliser la méthode statique se situe dans les
performances : le résultat est directement stocké dans la matrice de destination et ne passe pas par une
matrice intermédiaire, ce qui procure un gain de mémoire.

float aspectRatio = graphics.GraphicsDevice.Viewport.Width /


➥ graphics.GraphicsDevice.Viewport.Height;
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f, 1000f,
➥ out projection);

La matrice de vue
La deuxième étape concerne la matrice de vue. Vous utiliserez cette fois-ci la méthode
CreateLookAt () qui attend trois paramètres :
• la position de la caméra ;
• la direction visée par la caméra ;
• la rotation de la caméra.
Ces trois paramètres sont de type Vector3. Ainsi, dans l’extrait de code suivant, vous défi-
nissez les objets pour placer une caméra qui regarde vers l’origine du monde en 3D sans
rotation particulière.
Vector3 cameraPosition = new Vector3(0, 0, 3);
Vector3 cameraTarget = Vector3.Zero;
Vector3 cameraUpVector = Vector3.Up;
Là encore, il existe une version de la méthode statique qui attend comme dernier paramètre
la matrice résultat.
Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector, out
➥ view);
=Labat FM.book Page 267 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
267

Des vertices à la forme à dessiner


Nous allons à maintenant nous intéresser à l’utilisation concrète des vertices, d’abord
pour du dessin en deux dimensions, puis pour le dessin d’un cube.

Dessiner en 2D
Chaque vertex de la forme à dessiner à l’écran est défini par un objet de type Vertex
PositionColor. Vous stockerez tous ces vertices dans un tableau. Commencez par ajouter
un champ à la classe.
VertexPositionColor[] vertices;
La classe VertexPositionColor prend deux arguments : la position du vertex et sa couleur.
La méthode suivante crée un triangle :
private VertexPositionColor[] CreateShape()
{
VertexPositionColor[] vertices = new VertexPositionColor[3];

// Top left corner


vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White);

// Bottom right corner


vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White);

// Bottom left corner


vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White);

return vertices;
}
Vous n’avez plus qu’à dessiner le triangle. Commencez par signaler à l’objet Graphics
Device le type de vertex que vous allez utiliser.
Ensuite, déclarez un objet de type BasicEffect. Il faut savoir que, sans effet, vous ne pourrez
pas afficher de scène 3D à l’écran. Le premier paramètre attendu par le constructeur est
GraphicsDevice. Le second paramètre vous permet de partager des ressources entre
plusieurs effets. Fixez-le donc à null, puisque vous n’utilisez ici qu’un seul effet. Ensuite,
fixez les propriétés de l’objet, telle que la matrice de projection, celle de la vue, ou encore
le type de lumière désiré. Enfin, vous parcourrez les différentes passes de l’effet et appelez
la méthode DrawUserPrimitives() de GraphicsDevice. Cette méthode attend le type de
primitives à dessiner, les vertices à afficher, l’offset du premier vertex à dessiner et enfin,
le nombre de primitives à afficher.
Vous avez à présent tous les outils en main pour dessiner un premier triangle (figure 12-11).
Ci-dessous se trouve le code source complet de ce premier exemple.
=Labat FM.book Page 268 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


268

Figure 12-11
Le dessin d’une première forme

Premier dessin d’une forme


public class Chapitre12 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Matrix projection;
Matrix view;

Vector3 cameraPosition = new Vector3(0, 0, 3);


Vector3 cameraTarget = Vector3.Zero;
Vector3 cameraUpVector = Vector3.Up;

VertexPositionColor[] vertices;

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

protected override void Initialize()


{
float aspectRatio = graphics.GraphicsDevice.Viewport.Width /
➥ graphics.GraphicsDevice.Viewport.Height;
=Labat FM.book Page 269 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
269

Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f,


➥ 1000f, out projection);
Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref
➥ cameraUpVector, out view);

base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
vertices = CreateShape();
}
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);
graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration
➥ (graphics.GraphicsDevice, VertexPositionColor.VertexElements);

BasicEffect effect = new BasicEffect(graphics.GraphicsDevice, null);


effect.Projection = projection;
effect.View = view;
effect.LightingEnabled = false;
effect.Begin();
foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes)
{
CurrentPass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
➥ vertices, 0, vertices.Length / 3);
CurrentPass.End();
}
effect.End();
base.Draw(gameTime);
}
private VertexPositionColor[] CreateShape()
{
VertexPositionColor[] vertices = new VertexPositionColor[3];
// Top left corner
vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White);
=Labat FM.book Page 270 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


270

// Bottom right corner


vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White);

// Bottom left corner


vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White);

return vertices;
}
}

En pratique
La couleur utilisée ici pour effacer l’écran n’est pas le noir. En fait, il faut toujours éviter d’utiliser le noir
comme couleur de fond car si vous rencontrez des problèmes d’éclairage sur l’objet, celui-ci se confondra
avec le fond noir de l’écran et vous pourriez penser qu’il n’a pas été dessiné.

Vous auriez pu afficher les vertices sous une autre forme de primitive. Par exemple, sous
forme de points :
graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0,
➥ vertices.Length);
Ou bien sous forme d’une ligne continue :
graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0,
➥ vertices.Length - 1);

Figure 12-12
Les vertices sous forme d’une ligne continue
=Labat FM.book Page 271 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
271

Faites attention à l’ordre dans lequel les vertices doivent être dessinés. En effet, avec XNA,
la technique appelée backface culling, qui consiste à éliminer les faces d’un objet qui
tourne le dos à la caméra, est activée par défaut pour les vertices déclarés dans le sens
inverse des aiguilles d’une montre.
Ainsi la ligne suivante est implicitement déclarée :
graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
Si vous modifiez l’ordre des vertices de manière à les organiser dans le sens inverse des
aguilles d’une montre, vous verrez que plus rien ne s’affiche à l’écran.
// Top left corner
vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White);

// Bottom right corner


vertices[2] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White);

// Bottom left corner


vertices[1] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White);
Maintenant, si vous modifiez la propriété CullMode pour le mode qui cachera les faces dont
les vertices sont dans l’ordre des aiguilles d’une montre, le triangle apparaîtra correctement
à l’écran.
graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
Dans certains cas, vous n’aurez pas besoin d’utiliser la technique du backface culling,
vous pourrez alors la désactiver avec la valeur None de l’énumération CullMode ; l’ordre
des vertices n’aura plus d’importance.
graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;

Dessiner un cube
Maintenant que vous savez dessiner un triangle, vous pouvez vous consacrer à des formes
plus compliquées… et pourquoi pas un cube.
La première face
Avant de vous attaquer au cube complet, commencez par l’un de ses faces. Nous avons
vu précédemment qu’un rectangle est composé de deux triangles et qu’il est possible de
réutiliser les vertices du premier triangle pour dessiner le deuxième (deux vertices sont
communs aux deux triangles). Pour éviter de dupliquer ces points, il suffit d’utiliser un
index buffer. Ainsi, au lieu de dupliquer les vertices, vous dupliquez leurs index : vous
n’aurez que 4 vertices, mais bien 6 index.
1. Commencez par ajouter le nouveau vertex :
private VertexPositionColor[] CreateShape()
{
VertexPositionColor[] vertices = new VertexPositionColor[4];
=Labat FM.book Page 272 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


272

// Top left corner


vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White);

// Bottom right corner


vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White);

// Bottom left corner


vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White);

// Top right corner


vertices[3] = new VertexPositionColor(new Vector3(1, 1, 0), Color.White);

return vertices;
}
2. L’index buffer permet de définir l’ordre dans lequel vous souhaitez que les vertices
soient dessinés à l’écran. La méthode suivante place donc les index de manière à ce
que les deux triangles soient dessinés dans le sens des aiguilles d’une montre pour
ne pas avoir de problèmes avec le backface culling.
private short[] CreateIndices()
{
short[] indices = new short[6];

// Bottom triangle
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;

// Top triangle
indices[3] = 0;
indices[4] = 3;
indices[5] = 1;

return indices;
}
3. Ajoutez ensuite un champ à la classe et définissez-le dans la méthode LoadContent().
short[] indices;

protected override void LoadContent()


{
// …
indices = CreateIndices();
}
4. Pour dessiner le rectangle, vous n’utiliserez plus la méthode DrawUserPrimitives(),
mais la méthode DrawUserIndexedPrimitives(), qui prendra en compte l’index buffer.
graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList,
➥ vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
=Labat FM.book Page 273 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
273

5. Compilez le programme, le rectangle s’affiche correctement (figure 12-13).

Figure 12-13
Dessin d’un rectangle

Le cube complet
Vous avez maintenant toutes les clés pour dessiner un cube. Celui-ci doit être composé de
8 vertices. Faites bien attention lors de la création de l’index buffer. Pour ne pas faire
d’erreur, aidez-vous par exemple d’un croquis sur papier. Le constructeur de la classe
suivante attend également en paramètres un objet Vector3 pour placer le cube, ainsi que la
taille des arêtes des cubes.
Classe de création d’un cube
class Cube
{
VertexPositionColor[] vertices;
short[] indices;
BasicEffect effect;
GraphicsDevice graphicsDevice;
Vector3 position;
float widthOver2;

public Cube(GraphicsDevice graphicsDevice, Vector3 position, float width, Matrix


➥ projection, Matrix view)
{
this.graphicsDevice = graphicsDevice;
this.position = position;
this.widthOver2 = width / 2;
effect = new BasicEffect(graphicsDevice, null);
effect.Projection = projection;
=Labat FM.book Page 274 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


274

effect.View = view;
effect.LightingEnabled = false;
InitializeVertices();
InitializeIndices();
}
public void Draw()
{
graphicsDevice.VertexDeclaration = new VertexDeclaration(graphicsDevice,
➥ VertexPositionColor.VertexElements);
effect.Begin();
foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes)
{
CurrentPass.Begin();
graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList,
➥ vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
CurrentPass.End();
}
effect.End();
}
private void InitializeVertices()
{
vertices = new VertexPositionColor[8];
// Front Top left corner
vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
// Front Bottom right corner
vertices[1].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
// Front Bottom left corner
vertices[2].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
// Front Top right corner
vertices[3].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
// Back Top left corner
vertices[4].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
// Back Bottom right corner
vertices[5].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
// Back Bottom left corner
vertices[6].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
// Back Top right corner
=Labat FM.book Page 275 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
275

vertices[7].Position = new Vector3(position.X - widthOver2, position.Y +


➥ widthOver2, position.Z - widthOver2);
}

private void InitializeIndices()


{
indices = new short[36];

// Front
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;
indices[3] = 0;
indices[4] = 3;
indices[5] = 1;

// Right
indices[6] = 3;
indices[7] = 6;
indices[8] = 1;
indices[9] = 3;
indices[10] = 4;
indices[11] = 6;

// Top
indices[12] = 7;
indices[13] = 3;
indices[14] = 0;
indices[15] = 7;
indices[16] = 4;
indices[17] = 3;

// Back
indices[18] = 4;
indices[19] = 5;
indices[20] = 6;
indices[21] = 4;
indices[22] = 7;
indices[23] = 5;

// Left
indices[24] = 7;
indices[25] = 2;
indices[26] = 5;
indices[27] = 7;
indices[28] = 0;
indices[29] = 2;
// Bottom
indices[30] = 6;
indices[31] = 2;
=Labat FM.book Page 276 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


276

indices[32] = 1;
indices[33] = 6;
indices[34] = 5;
indices[35] = 2;
}
}

Plusieurs cubes
La classe ci-dessous dessine deux cubes en utilisant la classe précédente. Nous avons
modifié la position de la caméra afin de mieux voir les cubes : nous ne la plaçons plus
face au point d’origine, mais nous la décalons (figure 12-14).

Figure 12-14
Dessin de deux cubes

Exemple de dessin de plusieurs cubes


public class Chapitre12 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Matrix projection;
Matrix view;

Vector3 cameraPosition = new Vector3(5, 5, 5);


Vector3 cameraTarget = Vector3.Zero;
Vector3 cameraUpVector = Vector3.Up;

Cube cube;
=Labat FM.book Page 277 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
277

Cube cubeB;

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

protected override void Initialize()


{
float aspectRatio = graphics.GraphicsDevice.Viewport.Width /
➥ graphics.GraphicsDevice.Viewport.Height;
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f,
➥ 1000f, out projection);
Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref
➥ cameraUpVector, out view);

cube = new Cube(graphics.GraphicsDevice, Vector3.Zero, 1, projection, view);


cubeB = new Cube(graphics.GraphicsDevice, new Vector3(-4, 0, 0), 3,
➥ projection, view);

base.Initialize();
}

protected override void Draw(GameTime gameTime)


{
GraphicsDevice.Clear(Color.CornflowerBlue);

cube.Draw();
cubeB.Draw();

base.Draw(gameTime);
}
}

Déplacer la caméra
Maintenant que vous êtes capable de dessiner des formes 3D à l’écran, il faut encore que
vous permettiez au joueur de se déplacer dans ce monde. La caméra que nous allons créer
dans les lignes suivantes avancera si le joueur appuie sur la touche Haut, ou reculera si le
joueur presse la touche fléchée Bas, et tournera sur elle-même (autour de l’axe Y) si le
joueur presse les touches Gauche ou Droite.
Commencez par ajouter trois champs à la classe principale : un pour l’orientation de la
caméra, un pour la vitesse de déplacement de la caméra et un dernier pour la vitesse de
rotation de la caméra.
float cameraYaw = 0;
float moveSpeed = 0.1f;
float rotationSpeed = 0.05f;
=Labat FM.book Page 278 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


278

Dans un premier temps, mettez à jour la position de la caméra en fonction des entrées
utilisateur : translations sur les axes X et Z ou rotation autour de l’axe Y. Enfin, dernière
chose, mettez à jour la position visée par la caméra, ainsi que la matrice de vue. Le code
ci-dessous présente l’ensemble de la méthode Update() qui implémente maintenant la
nouvelle caméra.
Exemple de déplacement de la caméra
protected override void Update(GameTime gameTime)
{
Matrix movement = Matrix.CreateRotationY(cameraYaw);

if(Keyboard.GetState().IsKeyDown(Keys.Up) || Keyboard.GetState()
➥ .IsKeyDown(Keys.Down))
{
Vector3 vector = Vector3.Zero;

if (Keyboard.GetState().IsKeyDown(Keys.Up))
vector = new Vector3(0, 0, moveSpeed);
if (Keyboard.GetState().IsKeyDown(Keys.Down))
vector = new Vector3(0, 0, -moveSpeed);

vector = Vector3.Transform(vector, movement);


cameraPosition.Z += vector.Z;
cameraPosition.X += vector.X;
}

if (Keyboard.GetState().IsKeyDown(Keys.Left))
cameraYaw += rotationSpeed;
if (Keyboard.GetState().IsKeyDown(Keys.Right))
cameraYaw -= rotationSpeed;

if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();

cameraTarget = cameraPosition + Vector3.Transform(Vector3.Backward, movement);

Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector,


➥ out view);

base.Update(gameTime);
}
Il ne vous reste plus qu’à tester la nouvelle caméra (figure 12-15).

Pour aller plus loin


Sur le site de MSDN, vous trouverez plusieurs exemples de création de caméras, en vue subjective, à la
troisième personne… Il est également expliqué comment faire suivre une ligne à la caméra :
http://msdn.microsoft.com/en-us/library/bb203904.aspx/
=Labat FM.book Page 279 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
279

Figure 12-15
Parcourez la scène 3D

Appliquer une couleur à un vertex


Les cubes blancs ne sont pas des plus esthétiques, vous en conviendrez aisément. Il est
temps de les rendre plus joyeux en leur donnant de la couleur. Jusqu’à présent, vous avez
utilisé des vertices de type VertexPositionColor qui, comme leur nom l’indique, possèdent
deux propriétés : la première concerne la position dans l’espace et la seconde, la couleur.
Reprenez la méthode InitializeVertices() (un simple copier-coller suffit) de la classe
Cube et pour chaque vertex, modifiez la propriété Color :
private void InitializeVertices()
{
vertices = new VertexPositionColor[8];
// Front Top left corner
vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[0].Color = Color.White;
// Front Bottom right corner
vertices[1].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[1].Color = Color.Red;
// Front Bottom left corner
vertices[2].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
=Labat FM.book Page 280 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


280

vertices[2].Color = Color.Green;
// Front Top right corner
vertices[3].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[3].Color = Color.Yellow;
// Back Top left corner
vertices[4].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[4].Color = Color.Blue;
// Back Bottom right corner
vertices[5].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[5].Color = Color.Orange;
// Back Bottom left corner
vertices[6].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[6].Color = Color.Black;
// Back Top right corner
vertices[7].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[7].Color = Color.Violet;
}
N’oubliez pas d’activer la gestion des couleurs pour les vertices auprès de l’effet :
effect.VertexColorEnabled = true;
Le résultat de cette manipulation est visible sur la figure 12-16 : chaque sommet du cube
dispose de sa propre couleur et les couleurs se mélangent lorsqu’elles chevauchent celle
d’un vertex voisin.
Figure 12-16
Un cube coloré
=Labat FM.book Page 281 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
281

Plaquer une texture sur un objet


Le but de cette section est de vous apprendre ce que sont les coordonnées de texture,
comment appliquer une texture sur une face d’un objet, mais aussi comment l’appliquer
sur un objet tout entier.

Texturer une face d’un objet


Pour simplifier les choses, vous allez plaquer une première texture, non pas sur un cube,
mais sur une de ses faces. Nous allons travailler sur un simple carré.
La première chose à comprendre est la notion de coordonnées de texture. Le terme
« coordonnée de texture » désigne tout simplement une position au sein de la texture.
Cette position doit être comprise entre 0 et 1, le point d’origine étant placé sur le coin
supérieur gauche de la texture (figure 12-17). Ainsi, le coin inférieur gauche a pour coor-
données (0 ; 1) et le coin inférieur droit (1 ; 1).

Figure 12-17
Repère de coordonnées
de texture

Pour plaquer une texture, il faut associer chaque vertex à une paire de coordonnées de
texture. Ainsi, au lieu d’utiliser des vertices de type VertexPositionColor, nous allons
utiliser ceux de type VertexPositionTexture. Pour chaque vertex, définissez sa position à
l’écran via la propriété Position, mais aussi ses coordonnées de texture grâce à la propriété
TextureCoordinate.
Dans l’exemple suivant, à chaque sommet du carré, vous associez un coin de la texture.
private void InitializeVertices()
{
vertices = new VertexPositionTexture[8];
// Top left corner
vertices[0].Position = new Vector3(-1, 1, 0);
vertices[0].TextureCoordinate = new Vector2(0, 0);
// Bottom right corner
vertices[1].Position = new Vector3(1, -1, 0);
vertices[1].TextureCoordinate = new Vector2(1, 1);
=Labat FM.book Page 282 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


282

// Bottom left corner


vertices[2].Position = new Vector3(-1, -1, 0);
vertices[2].TextureCoordinate = new Vector2(0, 1);

// Top right corner


vertices[3].Position = new Vector3(1, 1, 0);
vertices[3].TextureCoordinate = new Vector2(1, 0);
}
Chargez la texture comme vous aviez l’habitude de le faire pour un jeu en 2D. La texture
que nous utilisons ici est un simple carré blanc où est écrit « XNA ».
Texture2D texture;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
texture = Content.Load<Texture2D>("texture");
}
N’oubliez pas de modifier l’objet VertexDeclaration pour qu’il prenne bien en compte un
objet de type VertexPositionTexture et non plus VertexPositionColor.
graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration
➥ (graphics.GraphicsDevice, VertexPositionTexture.VertexElements);
Dernière chose, signalez à l’effet que vous voulez texturer les vertices. Pour ce faire, passez
sa propriété TextureEnabled à true et passez à sa propriété Texture la texture à utiliser.
effect.TextureEnabled = true;
effect.Texture = texture;
Tout est prêt, compilez le programme et admirez le résultat à la figure 12-18.

Figure 12-18
Une première texture sur un objet en 3D
=Labat FM.book Page 283 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
283

Voyons comment affiner le positionnement de la texture. En jouant sur les indices, vous
affichez par exemple uniquement le quart supérieur gauche de la texture (figure 12-19).
private void InitializeVertices()
{
vertices = new VertexPositionTexture[8];
// Top left corner
vertices[0].Position = new Vector3(-1, 1, 0);
vertices[0].TextureCoordinate = new Vector2(0, 0);
// Bottom right corner
vertices[1].Position = new Vector3(1, -1, 0);
vertices[1].TextureCoordinate = new Vector2(0.5f, 0.5f);
// Bottom left corner
vertices[2].Position = new Vector3(-1, -1, 0);
vertices[2].TextureCoordinate = new Vector2(0, 0.5f);
// Top right corner
vertices[3].Position = new Vector3(1, 1, 0);
vertices[3].TextureCoordinate = new Vector2(0.5f, 0);
}

Figure 12-19
Une texture partiellement
affichée

Si les coordonnées de texture que vous passez aux vertices sont supérieures à 1, la texture
sera répétée (figure 12-20).
private void InitializeVertices()
{
vertices = new VertexPositionTexture[8];
// Top left corner
vertices[0].Position = new Vector3(-1, 1, 0);
vertices[0].TextureCoordinate = new Vector2(0, 0);
=Labat FM.book Page 284 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


284

// Bottom right corner


vertices[1].Position = new Vector3(1, -1, 0);
vertices[1].TextureCoordinate = new Vector2(2, 2);
// Bottom left corner
vertices[2].Position = new Vector3(-1, -1, 0);
vertices[2].TextureCoordinate = new Vector2(0, 2);
// Top right corner
vertices[3].Position = new Vector3(1, 1, 0);
vertices[3].TextureCoordinate = new Vector2(2, 0);
}

Figure 12-20
Une texture répétée

Nous avons vu à la section « Appliquer une couleur à un vertex » comment associer une
couleur à un vertex. En fait, les possibilités offertes sont plus vastes : vous pouvez associer
une texture et une couleur à un vertex ! Il vous faudra alors utiliser des objets de type
VertexPositionColorTexture au lieu des objets VertexPositionTexture.
private void InitializeVertices()
{
vertices = new VertexPositionColorTexture[8];

// Top left corner


vertices[0].Position = new Vector3(-1, 1, 0);
vertices[0].TextureCoordinate = new Vector2(0, 0);
vertices[0].Color = Color.Blue;

// Bottom right corner


vertices[1].Position = new Vector3(1, -1, 0);
=Labat FM.book Page 285 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
285

vertices[1].TextureCoordinate = new Vector2(2, 2);


vertices[1].Color = Color.Green;

// Bottom left corner


vertices[2].Position = new Vector3(-1, -1, 0);
vertices[2].TextureCoordinate = new Vector2(0, 2);
vertices[2].Color = Color.Red;

// Top right corner


vertices[3].Position = new Vector3(1, 1, 0);
vertices[3].TextureCoordinate = new Vector2(2, 0);
vertices[3].Color = Color.Yellow;
}

Cette fois encore, n’oubliez pas de modifier le VertexDeclaration, puis de signaler à l’effet
que les vertex utiliseront une couleur.
graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration
➥ (graphics.GraphicsDevice, VertexPositionColorTexture.VertexElements);
effect.VertexColorEnabled = true;

Le résultat est présenté à la figure 12-21. L’utilisation de la classe BasicEffect est d’une
facilité déconcertante, n’hésitez pas à y recourir.

Figure 12-21
Le carré coloré et texturé

Texturer un objet entier


Texturer un cube tout entier n’est pas aussi simple que texturer une seule de ses faces.
Reprenez la classe Cube écrite précédemment et utilisez des VertexPositionColorTexture
plutôt que des VertexPositionColor (figure 12-22).
=Labat FM.book Page 286 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


286

private void InitializeVertices()


{
vertices = new VertexPositionColorTexture[8];
// Front Top left corner
vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[0].Color = Color.White;
vertices[0].TextureCoordinate = new Vector2(0, 0);
// Front Bottom right corner
vertices[1].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[1].Color = Color.Red;
vertices[1].TextureCoordinate = new Vector2(2, 2);
// Front Bottom left corner
vertices[2].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[2].Color = Color.Green;
vertices[2].TextureCoordinate = new Vector2(0, 2);
// Front Top right corner
vertices[3].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[3].Color = Color.Yellow;
vertices[3].TextureCoordinate = new Vector2(2, 0);
// Back Top left corner
vertices[4].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[4].Color = Color.Blue;
vertices[4].TextureCoordinate = new Vector2(0, 0);
// Back Bottom right corner
vertices[5].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[5].Color = Color.Orange;
vertices[5].TextureCoordinate = new Vector2(2, 2);
// Back Bottom left corner
vertices[6].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[6].Color = Color.Black;
vertices[6].TextureCoordinate = new Vector2(0, 2);
// Back Top right corner
vertices[7].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[7].Color = Color.Violet;
vertices[7].TextureCoordinate = new Vector2(2, 0);
}
=Labat FM.book Page 287 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
287

Figure 12-22
Un problème lors du
placage de la texture

Il semble qu’il y ait un problème d’affichage… La raison est simple : vous n’avez que
8 vertices et les coordonnées de texture ont été définies de façon à bien correspondre pour
la face avant et la face arrière du cube. La solution est tout aussi simple : déclarez 4 vertices
pour chaque face du cube, soit 24 vertices au total, et paramétrez correctement les
coordonnées de texture pour chacune des faces :
private void InitializeVertices()
{
vertices = new VertexPositionColorTexture[24];

// Front Top left corner


vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[0].TextureCoordinate = new Vector2(0, 0);
vertices[0].Color = Color.White;

// Front Bottom right corner


vertices[1].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[1].TextureCoordinate = new Vector2(2, 2);
vertices[1].Color = Color.White;

// Front Bottom left corner


vertices[2].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[2].TextureCoordinate = new Vector2(0, 2);
vertices[2].Color = Color.White;

// Front Top right corner


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

Développement XNA pour la XBox et le PC


288

vertices[3].Position = new Vector3(position.X + widthOver2, position.Y +


➥ widthOver2, position.Z + widthOver2);
vertices[3].TextureCoordinate = new Vector2(2, 0);
vertices[3].Color = Color.White;

// Back Top left corner


vertices[4].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[4].TextureCoordinate = new Vector2(0, 0);
vertices[4].Color = Color.Blue;

// Back Bottom right corner


vertices[5].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[5].TextureCoordinate = new Vector2(2, 2);
vertices[5].Color = Color.Blue;

// Back Bottom left corner


vertices[6].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[6].TextureCoordinate = new Vector2(0, 2);
vertices[6].Color = Color.Blue;

// …
}
Nous aurons donc toujours 36 index. Cependant, ils ne seront plus répartis de la même
manière :
private void InitializeIndices()
{
// …
indices[24] = 16;
indices[25] = 17;
indices[26] = 18;
indices[27] = 16;
indices[28] = 19;
indices[29] = 17;

indices[30] = 20;
indices[31] = 21;
indices[32] = 22;
indices[33] = 20;
indices[34] = 23;
indices[35] = 21;
}
=Labat FM.book Page 289 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
289

Le résultat est visible sur la figure 12-23. Le fait qu’il n’y ait pas de texture affichée sur
la face du dessus et celle du dessous du cube est volontaire.

Figure 12-23
Un cube correctement texturé

En jouant avec les coordonnées de texture, vous pouvez vous arranger pour faire comme
si une texture différente était appliquée à chaque face du cube. Le cube de la figure 12-24
utilise cet effet.
private void InitializeVertices()
{
vertices = new VertexPositionColorTexture[24];

// Front Top left corner


vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[0].TextureCoordinate = new Vector2(0, 0);
vertices[0].Color = Color.White;
// Front Bottom right corner
vertices[1].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[1].TextureCoordinate = new Vector2(0.5f, 1);
vertices[1].Color = Color.White;
// Front Bottom left corner
vertices[2].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[2].TextureCoordinate = new Vector2(0, 1);
=Labat FM.book Page 290 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


290

vertices[2].Color = Color.White;
// Front Top right corner
vertices[3].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[3].TextureCoordinate = new Vector2(0.5f, 0);
vertices[3].Color = Color.White;
// …
// Right Top left corner
vertices[12].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[12].TextureCoordinate = new Vector2(0.5f, 0);
vertices[12].Color = Color.Red;
// Right Bottom right corner
vertices[13].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z - widthOver2);
vertices[13].TextureCoordinate = new Vector2(1, 1);
vertices[13].Color = Color.Red;
// Right Bottom left corner
vertices[14].Position = new Vector3(position.X + widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[14].TextureCoordinate = new Vector2(0.5f, 1);
vertices[14].Color = Color.Red;
// Right Top right corner
vertices[15].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);

Figure 12-24
Variation sur les coordonnées
de texture
=Labat FM.book Page 291 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
291

vertices[15].TextureCoordinate = new Vector2(1, 0);


vertices[15].Color = Color.Red;
// …
}
Dans l’extrait de code précédent, les coordonnées de texture sont appliquées de telle
façon que la texture apparaît à cheval sur deux face du cube. Le résultat est visible sur la
figure 12-24.

Déplacer un objet avec les transformations


À la section « Déplacer la caméra », nous avons vu comment nous promener dans un monde
en 3D en déplaçant la caméra. Nous allons à présent voir une autre méthode pour arriver
au même effet, mais en appliquant des transformations aux objets.
Prenez l’exemple du cube de la figure 12-24. Pour être en mesure de voir les faces cachées,
vous appliquez une rotation au cube autour de l’axe Y.
Ajoutez une méthode Update() à la classe. Dans cette méthode, vous définissez la matrice
de rotation grâce à la méthode CreateRotationY de la classe Matrix, puis vous appliquez le
résultat à la propriété World de l’effet :
public void Update(GameTime gameTime)
{
rotation = * Matrix.CreateRotationY(0.3f * (float)gameTime
➥ .TotalGameTime.TotalSeconds);
}
Compilez le projet : le cube tourne à une vitesse modérée autour de l’axe Y. L’exemple
de code ci-dessous utilise des rotations autour des axes X, Y et Z.
public void Update(GameTime gameTime)
{
effect.World = Matrix.CreateRotationX(0.3f *
➥ (float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateRotationY(0.3f * (float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateRotationZ(0.3f * (float)gameTime.TotalGameTime.TotalSeconds);

}
La figure 12-25 montre le résultat de l’extrait de code précédent.
De la même manière, modifiez la taille du cube grâce à la fonction CreateScale().
L’exemple de code suivant fait grandir le cube en fonction du temps (figure 12-26).
public void Update(GameTime gameTime)
{
effect.World = Matrix.CreateScale(0.03f * (float)gameTime
➥ .TotalGameTime.TotalSeconds);
}
=Labat FM.book Page 292 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


292

Figure 12-25
Un cube qui tourne autour des axes X, Y et Z

Figure 12-26
Un cube qui grossit avec le temps
=Labat FM.book Page 293 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
293

Enfin, vous pouvez appliquer des translations au cube grâce à la méthode Create
Translation (). Cette méthode attend un objet de type Vector3. L’exemple suivant applique
des translations au cube en fonction des entrées clavier de l’utilisateur.
public void Update(GameTime gameTime)
{
Vector3 translation = Vector3.Zero;

if (Keyboard.GetState().IsKeyDown(Keys.Right))
translation = new Vector3(0.1f, 0, 0);

if (Keyboard.GetState().IsKeyDown(Keys.Left))
translation = new Vector3(-0.1f, 0, 0);

if (Keyboard.GetState().IsKeyDown(Keys.Up))
translation = new Vector3(0, 0, -0.1f);

if (Keyboard.GetState().IsKeyDown(Keys.Down))
translation = new Vector3(0, 0, 0.1f);

effect.World *= Matrix.CreateTranslation(translation);
}

Bien entendu, vous pouvez combiner les différentes transformations. Par exemple,
l’extrait de code suivant combine translations et modification de l’échelle (figure 12-27).

Figure 12-27
Le cube grossit et se déplace
=Labat FM.book Page 294 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


294

public void Update(GameTime gameTime)


{
Vector3 translation = Vector3.Zero;

if (Keyboard.GetState().IsKeyDown(Keys.Right))
translation = new Vector3(0.1f, 0, 0);

if (Keyboard.GetState().IsKeyDown(Keys.Left))
translation = new Vector3(-0.1f, 0, 0);

if (Keyboard.GetState().IsKeyDown(Keys.Up))
translation = new Vector3(0, 0, -0.1f);

if (Keyboard.GetState().IsKeyDown(Keys.Down))
translation = new Vector3(0, 0, 0.1f);

effect.World *= Matrix.CreateTranslation(translation);
effect.World += Matrix.CreateScale(0.1f *
➥ (float)gameTime.TotalGameTime.TotalSeconds);
}

Jouer avec les lumières


Jusqu’à présent, nous avons laissé de côté un point important, mais il prend toute sa
signification dans le dessin d’une scène en 3D de qualité : la lumière. C’est grâce à elle
que vous rendrez une scène plus vivante, en lui donnant une certaine ambiance et en
imaginant des mécanismes du gameplay basés sur elle.

Les différents types de lumière


La première chose à savoir est qu’il existe plusieurs types de lumière :
• La lumière diffuse (diffuse lighting), qui vient d’une direction particulière. L’intensité
de ce type de lumière dépend de l’angle entre la surface de l’objet éclairé et la direction
de la lumière : plus l’angle se rapproche de 90°, plus la lumière est réfléchie. Après
avoir rencontré une surface, elle est renvoyée uniformément dans toutes les directions.
• La lumière spéculaire (specular light), qui vient d’une direction particulière et qui repart
dans une direction particulière. Ce genre de lumière peut généralement se repérer par
un petit point intensément brillant.
• La lumière d’ambiance (ambient light), c’est-à-dire le type de lumière qui ne semble
pas venir d’une source en particulier, mais qui semble faire partie de l’environnement.
Elle permet aux objets de ne pas sembler totalement noirs.
Pour utiliser une lumière diffuse ou une lumière spéculaire, vous devez connaître l’angle
entre la surface d’un objet et la direction des rayons de lumière. Ce n’est pas la peine de
vous saisir de votre rapporteur, l’effet se chargera de ce travail. La seule chose que vous
=Labat FM.book Page 295 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
295

devrez lui spécifier, c’est la direction vers laquelle la surface est orientée, c’est-à-dire la
direction perpendiculaire à la surface : sa normale.

Éclairer une scène pas à pas


Reprenez la classe Cube et modifiez-la de manière à utiliser la structure VertexPosition
NormalTexture pour les vertices. L’extrait de code ci-dessous repose sur la méthode
InitializeVertices(). Il présente la définition de la normale d’un seul vertex de chaque
face (elle est la même pour tous les vertices d’une face).
private void InitializeVertices()
{
vertices = new VertexPositionNormalTexture[24];

// Front Top left corner


vertices[0].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[0].TextureCoordinate = new Vector2(0, 0);
vertices[0].Normal = new Vector3(0, 0, 1);

// Back Top left corner


vertices[4].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[4].TextureCoordinate = new Vector2(0, 0);
vertices[4].Normal = new Vector3(0, 0, -1);

// Left Top left corner


vertices[8].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[8].TextureCoordinate = new Vector2(0, 0);
vertices[8].Normal = new Vector3(-1, 0, 0);

// Right Top left corner


vertices[12].Position = new Vector3(position.X + widthOver2, position.Y +
➥ widthOver2, position.Z + widthOver2);
vertices[12].TextureCoordinate = new Vector2(0.5f, 0);
vertices[12].Normal = new Vector3(1, 0, 0);

// Bottom Top left corner


vertices[16].Position = new Vector3(position.X - widthOver2, position.Y -
➥ widthOver2, position.Z + widthOver2);
vertices[16].Normal = new Vector3(0, -1, 0);

// Top Top left corner


vertices[20].Position = new Vector3(position.X - widthOver2, position.Y +
➥ widthOver2, position.Z - widthOver2);
vertices[20].Normal = new Vector3(0, 1, 0);
}
=Labat FM.book Page 296 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


296

Commencez par activer la prise en charge de la lumière auprès de l’effet :


effect.LightingEnabled = true;
Le cube a disparu : c’est tout à fait normal puisque vous n’avez pas encore défini de
lumières. Commencez par vous occuper de la lumière d’ambiance en définissant la propriété
AmbientLightColor de l’effet. Cette propriété est de type Vector3 : la composante X correspond
au niveau de rouge, la composante Y au niveau de vert et la composante Z au niveau de
bleu. Ces trois composantes doivent être définies par un nombre compris entre 0 et 1.
La ligne suivante définit une lumière d’ambiance blanche (figure 12-28).
effect.AmbientLightColor = new Vector3(1, 1, 1);

Figure 12-28
Une lumière d’ambiance
blanche

Le rendu suivant fait clairement apparaître qu’une lumière d’ambiance ne doit pas être
trop présente. Préférez un gris plus sombre (même quantité de rouge, de vert et de bleu).
Le résultat visible sur la figure 12-29 semble être assez concluant.
effect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
La classe BasicEffect vous permet d’ajouter jusqu’à trois lumières directionnelles (qu’elles
soient diffuses ou spéculaires) : DirectionalLight0, DirectionalLight1 et DirectionalLight2.
Dans un premier temps, vous allez utiliser une lumière diffuse. Commencez par activer la
lumière directionnelle grâce à sa propriété Enabled. Donnez-lui ensuite sa direction via un
objet de type Vector3 (dans l’exemple suivant, elle est dirigée vers le bas et vers le fond
de la scène). Enfin, choisissez la couleur qu’elle doit diffuser (figure 12-30).
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight0.Direction = Vector3.Normalize(new Vector3(0, -1, -1));
effect.DirectionalLight0.DiffuseColor = new Vector3(1f, 1f, 1f);
=Labat FM.book Page 297 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
297

Figure 12-29
Une lumière d’ambiance
moins prononcée

Figure 12-30
Une lumière diffuse
qui éclaire le cube

Maintenant, ajoutez une lumière spéculaire à la scène. Activez la lumière directionnelle


DirectionalLight1, donnez-lui une direction (dans l’exemple suivant, vers la gauche et vers
le bas) et enfin, renseignez la couleur qui doit être utilisée via la propriété SpecularColor.
Dans l’exemple suivant, la lumière spéculaire éclaire en vert (figure 12-31).
effect.DirectionalLight1.Enabled = true;
effect.DirectionalLight1.Direction = Vector3.Normalize(new Vector3(-1, -1, 0));
effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
=Labat FM.book Page 298 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


298

Figure 12-31
Combinaison de lumière
ambiante, lumière diffuse
et lumière spéculaire

La classe BasicEffect permet également d’ajouter facilement du brouillard aux scènes.


Commencez par l’activer avec la propriété FogEnabled, puis définissez la couleur du
brouillard avec la propriété FogColor. Dernière chose, définissez la limite proche et la
limite distante du brouillard avec les propriétés FogStart et FogEnd. Tout ce qui sera avant
FogStart sera affiché normalement, tout ce qui sera après FogEnd ne sera plus visible.
effect.FogEnabled = true;
effect.FogColor = new Vector3(0.5f, 0.5f, 0.5f);
effect.FogStart = 5;
effect.FogEnd = 6;
Dernière chose, pour renforcer l’effet, modifiez la couleur de fond du jeu pour la faire
coïncider avec celle du brouillard et faire disparaître une partie du cube (figure 12-32).
GraphicsDevice.Clear(new Color(127,127,127));

Figure 12-32
Le cube est maintenant
noyé dans le brouillard :
la présence des lumières
est diminuée
=Labat FM.book Page 299 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
299

Charger un modèle
Rassurez-vous, dans un jeu en 3D, vous ne passerez pas votre temps à coder des vertices
pour obtenir des triangles et former les objets que vous désirez. Nous allons voir dans la
dernière partie de ce chapitre comment charger et afficher à l’écran un modèle en 3D. Les
modèles sont des objets constitués de polygones créés dans des modeleurs 3D.
Les modèles et objets 3D qui peuvent être chargés par défaut par XNA doivent être au
format .x ou .fbx. Pour créer de tels objets, vous pouvez utiliser des modeleurs 3D open
source tels que Blender (http://www.blender.org/) ou encore TrueSpace (http://www.caligari.com/)
ou, si vous êtes un artiste 3D et que vous possédez une licence d’un modeleur professionnel
tel que 3ds max, Cinema 4D, etc., rien ne vous empêche de les utiliser. Assurez-vous juste
de posséder un exportateur de fichier .x (c’est généralement le cas).

Références
O. Saraja. La 3D libre avec Blender, 3e édition, éditions Eyrolles, 2008.
J.-P. Couwenbergh. 3ds max 2008, éditions Eyrolles, 2008.
C. Blazy et E. Roux. Cinema 4D, éditions Eyrolles, 2006.

Dans l’exemple suivant, nous utilisons un vaisseau du starter kit Space War. Un modèle
s’ajoute au projet exactement de la même manière qu’une texture ou une autre ressource.
Pour utiliser un modèle, pensez à la classe Model. Vous chargerez ensuite le modèle grâce
à la méthode Load() du Content Manager.
Model model;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
model = Content.Load<Model>("Models\\p1_wedge");
}

Pour rendre les choses un peu plus jolies, faites tourner le modèle.
float rotation = 0.0f;
protected override void Update(GameTime gameTime)
{
rotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds *
➥ MathHelper.ToRadians(0.1f);
base.Update(gameTime);
}

Un modèle pouvant être composé de plusieurs mesh, vous devrez les parcourir, puis les
dessiner un par un avec la méthode Draw(). Chaque mesh peut également posséder plusieurs
effets, pensez à les parcourir eux aussi :
protected override void Draw(GameTime gameTime)
{
=Labat FM.book Page 300 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


300

graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

foreach (ModelMesh mesh in model.Meshes)


{
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = Matrix.CreateRotationY(rotation);
effect.View = view;
effect.Projection = projection;
}
mesh.Draw();
}
base.Draw(gameTime);
}
Après avoir compilé le projet, vous constatez que ce modèle s’affiche correctement et
qu’il tourne sur lui-même (figure 12-33).

Figure 12-33
Votre premier modèle chargé et dessiné à l’écran

Vous pouvez modifier la caméra et ajouter un fond à la scène pour obtenir un rendu similaire
à celui présenté à la figure 12-34.
=Labat FM.book Page 301 Vendredi, 19. juin 2009 4:01 16

Les bases de la programmation 3D


CHAPITRE 12
301

Figure 12-34
En avant vers un vrai jeu en 3D

En résumé
Vous connaissez à présent les bases nécessaires à la création d’un jeu vidéo en 3D :
• les bases théoriques de la 3D (vertices, vecteurs, etc.) ;
• comment dessiner des primitives ;
• comment déplacer une caméra dans une scène 3D ;
• comment appliquer une couleur ou une texture à un vertex ;
• comment déplacer les objets ;
• les différents types de lumière dans XNA et comment les gérer ;
• comment charger un modèle en 3D.
Le chapitre suivant est consacré au HLSL et à la réalisation d’effets, ce qui vous permettra
d’améliorer encore la qualité graphique de vos jeux.
=Labat FM.book Page 302 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 303 Vendredi, 19. juin 2009 4:01 16

13
Améliorer le rendu
avec le High Level
Shader Language

Certains diront que les effets graphiques sont l’ingrédient indispensable à la réalisation
d’un bon jeu, d’autres répliqueront qu’il ne s’agit que de fioritures visuelles qui exigent
du matériel graphique de plus en plus puissant. Cependant, impossible de faire l’impasse
sur des termes comme glow, blur, ou bloom. En effet, aujourd’hui, toutes les grandes
productions en usent, et parfois en abusent.
Ce chapitre présente les shaders ainsi que le langage que vous utiliserez pour les
programmer, à savoir le HLSL. Vous découvrirez aussi plusieurs exemples de shaders et
comment les utiliser dans un projet XNA.
La première partie de ce chapitre explique ce que sont les shaders et présente le HLSL.
Vous découvrirez ensuite la syntaxe du HLSL.

Les shaders et XNA


Un shader est un programme directement exécutable par la carte graphique. Il permet de
transférer la gestion des lumières, ombres, textures, etc., au processeur graphique et de
soulager de cette charge de travail le processeur de l’ordinateur. Ainsi, à partir du même
contenu de base, mais en utilisant divers shaders, vous pouvez obtenir des rendus complè-
tement différents.
=Labat FM.book Page 304 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


304

Vertex shaders et pixel shaders


On distingue deux types de shaders : les vertex shaders et les pixels shaders.
Les vertex shaders sont exécutés sur tous les vertices d’un objet. Si vous dessinez un
simple triangle à l’écran, le vertex shader sera exécuté pour les trois vertices qui composent
ce triangle. Mais les vertex shaders peuvent également être exécutés sur des sprites en 2D :
en effet, ceux-ci sont constitués de 4 vertices (un dans chaque coin du sprite).
Quant aux pixels shaders, ils sont exécutés sur tous les pixels visibles à l’écran de l’objet
que vous dessinez, qu’il s’agisse d’un objet en 3D ou d’un sprite en 2D.
Chronologiquement, le processus est le suivant :
1. Le GPU (processeur graphique) reçoit des paramètres et des vertices (leur position,
couleur, texture, etc., tout dépend du choix que vous avez fait).
2. Le vertex shader est d’abord exécuté. En sortie, vous disposez des vertices modifiés
(ou non, tout dépend du shader).
3. Ces données passent ensuite à l’étape de rastérisation. Au cours de cette étape, les
données vectorielles sont converties en données composées de pixels pouvant être
affichés à l’écran.
4. Le résultat de la rastérisation est ensuite envoyé au pixel shader. Après l’exécution
de ce programme, le GPU retourne la couleur qui devra être affichée.
Comme pour un processeur classique, le processeur graphique ne comprend que des
instructions de bas niveau. Ainsi, pour faciliter le travail des développeurs, des langages
de plus haut niveau (c’est-à-dire, plus intelligibles) à la syntaxe très proche des langages
tels que le C ont été créés. Du côté d’OpenGL, le langage standardisé est le GLSL. Le
fabricant de cartes graphiques NVidia a, quant à lui, créé le langage Cg. Enfin, de son
côté, Microsoft a créé le HLSL (High Level Shader Language). C’est ce dernier langage
qui est utilisé par l’API DirectX, et donc par XNA.

Définition
OpenGL est une API multi-plate-forme d’affichage en trois dimensions développée par Silicon Graphics.

Dans XNA, pour afficher quelque chose à l’écran vous devez passer obligatoirement
par HLSL. Cependant, pour vous faciliter les choses (c’est le mot d’ordre de XNA après
tout !) et vous éviter de devoir vous attaquer directement au HLSL, les développeurs ont créé
les classes SpriteBatch et BasicEffect. En arrière-plan, ces classes travaillent directement
avec des shaders. Vous pouvez d’ailleurs retrouver le code source de ces shaders à cette
adresse : http://creators.xna.com/fr-FR/education/catalog/.
=Labat FM.book Page 305 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
305

Ajouter un fichier d’effet dans XNA


Les fichiers d’effets écrits en HLSL sont automatiquement gérés par le Content Manager
de XNA. Exactement comme vous ajoutez n’importe quel type de fichier (une police de
caractères par exemple), vous pouvez ajouter un fichier d’effet à XNA (figure 13-1).

Figure 13-1
Ajout d’un fichier .fx au projet

Vous avez ensuite toute latitude pour éditer le fichier .fx directement dans Visual Studio,
avec un bloc-notes ou bien, si vous désirez des fonctionnalités plus avancées, notamment
pour déboguer un effet, via FX Composer de NVidia (figure 13-2).

FX Composer
Téléchargez FX Composer à l’adresse suivante :
http://developer.nvidia.com/object/fx_composer_home.html/
Puis, installez le programme comme une application classique.
=Labat FM.book Page 306 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


306

Figure 13-2
FX Composer, l’éditeur de shaders de NVidia

Syntaxe du langage HLSL


Vous allez maintenant découvrir la syntaxe de base du langage HLSL. Celle-ci étant très
proche des langages C/C++/C#, vous l’assimilerez très vite.

Les variables HLSL


Comme la plupart des langages de programmation, le HLSL permet de déclarer des
variables de différents types. Vous pouvez ainsi utiliser des variables de type int, float,
bool, etc., et même définir vos propres types grâce aux structures.

Définir la matrice
Pour définir les matrices, deux manières différentes s’offrent à vous en HLSL :
=Labat FM.book Page 307 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
307

• La première manière consiste à utiliser le mot-clé floatLxC, où L est le nombre de


lignes et C, le nombre de colonnes. La ligne de code suivante sert donc à déclarer une
matrice de 4 colonnes par 4 lignes.
float4x4 myMatrix;
• La seconde repose sur le mot-clé matrix auquel vous ajoutez le type des données et les
dimensions de la matrice :
matrix<float,4,4> myMatrix;

Accéder au contenu de la matrice


Voici les deux manières d’accéder au contenu de la matrice :
• La première consiste à considérer la matrice comme un tableau de tableaux :
float element = myMatrix[0][0];
• La deuxième repose sur l’utilisation de la notation pointée « . », comme vous le faites
en C# pour accéder aux membres d’une structure ou d’une classe :
float element = myMatrix._m23;
À la différence du C#, il n’existe pas de types Vector2, Vector3, etc. Vous utiliserez donc les
types vector ou floatX, où X est le nombre de composantes utilisées. Ainsi, vous stockerez
la position d’un vertex dans une variable de type vector ou float3.

Accéder aux composantes d’un vecteur


L’accès aux différentes composantes d’un vecteur se fait de deux manières :
• La première consiste à utiliser les crochets [], comme vous le feriez pour un tableau en
C#.
float value = myVector[0];
• La deuxième consiste à utiliser la notation pointée « . ». Les composantes peuvent
être identifiées par deux noms différents. La première série de noms est rgba et la
seconde, xyzw.
Par exemple, si vous avez un vecteur color, vous pouvez accéder à son niveau de
vert des deux manières suivantes :
float green = color.g;
float green = color.y;
Le swizzling sert à accéder à plusieurs composantes d’un vecteur en même temps. Dans
les deux exemples suivants (qui utilisent les deux méthodes d’accès aux composantes),
vous accédez aux composantes rouge et verte d’une couleur :
float2 redGreen = { color[0],color[1]};
float2 redGreen = color.rg;
=Labat FM.book Page 308 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


308

Attention à l’erreur !
L’exemple de code suivant est invalide : vous ne pouvez pas mélanger les noms des deux groupes de
noms de composantes.
float2 redGreen = color.xg;

Les structures de contrôle


Les structures de contrôle vous permettont d’effectuer des traitements avancés pour
aboutir à des effets encore plus réussis. Vous retrouvez en HLSL une grande partie des
structures de contrôle que vous connaissiez déjà en C# et qui s’utilisent de la même manière,
à savoir :
• If (si condition vérifiée, alors) ;
• While (tant que condition vérifiée, faire) ;
• Do (faire tant que condition vérifiée) ;
• For (pour X de I a J, faire).

Les fonctions fournies pas le langage


Le langage HLSL fournit une très longue liste de fonctions faciles à utiliser dans les shaders.
Le tableau 13-1 en cite quelques-unes. La liste complète se trouve dans la documentation
de DirectX, à laquelle vous accéderez via la bibliothèque MSDN (voir annexe B).

Tableau 13-1 Exemples de fonctions fournies pas le langage

Fonction Description
Cos(x) Cosinus de x.

Sin(x) Sinus de x.

Mul(a,b) Multiplication de la matrice a avec la matrice b.

Pow(x,y) Retourne x à la puissance y.

Sémantiques et structures pour formats d’entrée et de sortie


Les sémantiques servent à lier les entrées et sorties d’une fonction. Par exemple, elles
servent à lier la sortie de l’application XNA à l’entrée du vertex shader. Dans le processus
de rastérisation, d’autres sémantiques lient la sortie du vertex shader avec l’entrée. Pour
finir cette courte présentation, signalons également que le pixel shader reçoit et renvoie
lui aussi des sémantiques.
Vous déclarez vos propres formats d’entrée et de sortie en utilisant les structures. L’exemple
ci-dessous définit le format d’entrée pour un vertex shader.
=Labat FM.book Page 309 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
309

struct VertexInput
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
Dans la structure précédente, vous constatez que deux sémantiques sont utilisées : une pour
la position du vertex et l’autre pour les coordonnées de texture qui lui sont associées. La
liste complète des sémantiques disponibles se trouve dans la documentation de DirectX,
le tableau 13-2 présente un court extrait de cette liste.

Tableau 13-2 Sémantiques d’entrée pour vertex shader

Sémantique Description
COLOR Couleur diffuse ou spéculaire.

NORMAL Normale au vertex.

POSITION Position du vertex.

TEXCOORD Coordonnées de texture associées au vertex.

Procédez de la même manière pour les données en sortie du vertex shader (VertexOutput),
les données en entrée du pixel shader (PixelInput) et celles en sortie du pixel shader
(PixelOutput). Attention, la liste des sémantiques n’est pas la même pour chacune de ces
étapes.

Incompatibilité de versions
Certaines sémantiques ne sont pas toujours valables ou ne s’utilisent pas toujours de la même façon selon
la version des vertex shaders ou pixel shaders. Vous devrez donc faire attention à la version des shaders
que vous utiliserez lors de la compilation.

Écrire un vertex shader


Vous savez à présent tout ce qu’il faut savoir pour écrire un premier vertex shader. Il
s’agit d’une simple fonction qui doit retourner un objet de type VertexOutput, qui est une
structure que vous avez normalement déclarée.
VertexOutput vertexShader(VertexInput input)
{
VertexOutput output;
WorldViewProjection = mul(mul(World, View), Projection);
output.Position = mul(Pos, WorldViewProjection);
output.TexCoord = input.TexCoord;
return(output);
}
=Labat FM.book Page 310 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


310

Ce premier vertex shader multiplie les différentes matrices (World, View et Projection)
pour transformer la position du vertex, puis recopier les coordonnées de textures.

Écrire un pixel shader


Le pixel shader est une fonction qui retourne une couleur, c’est-à-dire un vecteur à
4 dimensions.
float4 pixelShader(PixelInput input) : COLOR
{
return tex2D(TextureSampler, input.TexCoord);
}
Le pixel shader ci-dessus retourne la couleur de la texture aux coordonnées de texture de
l’objet PixelInput.

Finaliser un effet : les techniques et les passes


Comme nous l’avons au chapitre précédent, un fichier d’effets peut contenir plusieurs
techniques. Une technique n’est rien de plus qu’un nom et un conteneur de passes. Une
passe définit quel vertex shader et quel pixel shader doivent être utilisés. C’est aussi ici
que vous choisissez la version des shaders (dans le cas présent, version 1 pour les deux).
technique Default
{
pass P0
{
VertexShader = compile vs_1_1 vertexShader();
PixelShader = compile ps_1_1 pixelShader();
}
}
Vous savez à présent ce qui se cache derrière les termes de vertex shader ou pixel shader.
Vous connaissez également la syntaxe du langage HLSL. Bref, vous avez toutes les clés
en main pour écrire vos premiers fichiers d’effet.

Créer le fichier d’effet


En reprenant les différents éléments de base d’un fichier d’effets que nous avons vus dans
la première partie de ce chapitre, vous reconstituez le fichier suivant. Ce premier fichier
d’effet affiche le modèle à l’écran, ni plus, ni moins.
Fichier d’effet basique
float4x4 World : WORLD;
float4x4 View;
float4x4 Projection;
float4x4 WorldViewProjection : WORLDVIEWPROJECTION;
texture Texture;
=Labat FM.book Page 311 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
311

sampler TextureSampler = sampler_state


{
texture = <Texture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = mirror;
AddressV = mirror;
};

struct VertexInput
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};

struct VertexOutput
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};

VertexOutput vertexShader(VertexInput input)


{
VertexOutput output;
WorldViewProjection = mul(mul(World, View), Projection);
output.Position = mul(input.Position, WorldViewProjection);
output.TexCoord = input.TexCoord;
return( output );
}

struct PixelInput
{
float2 TexCoord : TEXCOORD0;
};

float4 pixelShader(PixelInput input) : COLOR


{
return tex2D(TextureSampler, input.TexCoord);
}

technique Default
{
pass P0
{
VertexShader = compile vs_1_1 vertexShader();
PixelShader = compile ps_1_1 pixelShader();
}
}
=Labat FM.book Page 312 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


312

Voyons comment complexifier les choses… Dans un projet XNA, reprenez la classe Cube
écrite au chapitre précédent. Modifiez la classe de manière à ne plus utiliser la classe
BasicEffect, mais la classe Effect :
Effect effect;
Chargez ensuite l’effet grâce au Content Manager, comme si vous chargiez une texture,
un modèle, etc.
effect = Content.Load<Effect>("FirstEffect");
Vous accédez aux différentes variables globales de l’effet via la propriété Parameters. Les
variables sont ensuite indexées selon leur nom. Renseignez donc les variables World, View,
Projection et Texture.
effect.Parameters["Projection"].SetValue(projection);
effect.Parameters["View"].SetValue(view);
effect.Parameters["World"].SetValue(Matrix.Identity);
effect.Parameters["Texture"].SetValue(texture);

Plusieurs techniques dans le même fichier d’effet


Si vous aviez eu plusieurs techniques dans le fichier, vous auriez pu définir la propriété CurrentTechnique
grâce à la propriété Techniques, où les techniques sont indexées par leur nom.

effect.CurrentTechnique = effect.Techniques["Default"];

Tout est prêt, vous pouvez compiler le projet et le lancer : le cube s’affiche correctement.
Pour compliquer encore un peu les choses, modifions ce premier fichier afin qu’il prenne
en charge la lumière ambiante.
Commencez par ajouter une variable globale dans le fichier. Cette variable devra contenir
la couleur de la lumière d’ambiance.
float4 AmbientColor : COLOR0;
Vous n’avez plus qu’à appliquer la lumière d’ambiance dans le pixel shader.
float4 pixelShader(PixelInput input) : COLOR
{
return (tex2D(TextureSampler, input.TexCoord) * AmbientColor);
}
Le fichier d’effet est maintenant prêt. Repassez dans le fichier cube.cs et définissez la
variable AmbientColor.
effect.Parameters["AmbientColor"].SetValue(0.5f);
=Labat FM.book Page 313 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
313

Une dernière petite modification pour finir ce premier fichier d’effet : nous allons afficher
le cube en mode fil de fer (figure 13-3). Pour cela, il suffit d’ajouter la ligne suivante dans
la passe :
FillMode = Wireframe;

Figure 13-3
Le cube en mode fil de fer

De la même manière, vous pouvez désactiver le culling, ce qui fera bien apparaître toutes
les faces du cube (figure 13-4).
CullMode = none;

Figure 13-4
Le cube en mode fil de fer sans culling
=Labat FM.book Page 314 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


314

Faire onduler les objets


Le premier fichier d’effets que vous venez de réaliser n’a rien d’extraordinaire. Vous allez
maintenant créer un effet plus intéressant que ceux proposés par la classe BasicEffect.
Le but de l’effet suivant est de donner un effet de vague à des vertices : celui-ci oscillera
en fonction du temps grâce à la fonction sinus.
1. Commencez par ajouter une nouvelle variable globale au fichier qui servira à stocker
le temps.
float Timer : TIME;
2. Modifiez ensuite le vertex shader pour qu’il change les coordonnées du vertex, qui est
quant à lui passé en paramètre. Pour cela, copiez sa position dans un vecteur temporaire.
3. Modifiez la composante x, le calcul utilise la fonction sinus qui prend en paramètre
la composante y et le temps courant.
4. Dernière chose, passez ce jeu de coordonnées mis à jour au vertex de sortie.
VertexOutput vertexShader(VertexInput input)
{
float4 Pos = float4(input.Position.xyz,1);
Pos.x += sin(Pos.y + Timer);

VertexOutput output;
WorldViewProjection = mul(mul(World, View), Projection);
output.Position = mul(Pos, WorldViewProjection);
output.TexCoord = input.TexCoord;
return( output );
}
5. Dans le code C#, à chaque passage dans la méthode Draw(), passez le nombre de
secondes écoulées au total.
effect.Parameters["Timer"].SetValue((float)gameTime.TotalGameTime.TotalSeconds);
Vous pouvez ensuite essayer le projet et admirer le résultat (figure 13-5).

Figure 13-5
Le cube déformé
par le nouvel effet
=Labat FM.book Page 315 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
315

La texture en négatif
À présent, voyons comment modifier le pixel shader de manière à ce que le cube appa-
raisse en négatif (figure 13-6). Pour cela, vous devez effectuer une simple inversion des
couleurs : soustrayez chacune des composantes de la couleur du pixel à 1.
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = 1 - tex2D(TextureSampler, input.TexCoord);
return( color );
}

Figure 13-6
Le cube est maintenant affiché en négatif

Jouer avec la netteté d’une texture


Le pixel shader suivant fait varier la netteté de l’image (figure 13-7). Il suffit de modifier
la couleur du pixel courant en fonction des pixels voisins.
1. Commencez par déclarer une variable globale dans le fichier d’effets.
float SharpAmount;
2. Puis, utilisez le pixel shader suivant.
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = tex2D( TextureSampler, input.TexCoord);
color += tex2D( TextureSampler, input.TexCoord - 0.0001) * SharpAmount;
color -= tex2D( TextureSampler, input.TexCoord + 0.0001) * SharpAmount;
return( color );
}
=Labat FM.book Page 316 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


316

Figure 13-7
Modification de la netteté du cube

3. Pour apprécier pleinement l’impact de cet effet sur le cube, ajoutez une variable au
projet C#.
float sharpAmount = 0;
4. Pour faire varier l’ampleur de l’effet, augmentez la valeur de la variable précédente si
le joueur appuie sur la touche PageUp et faites-la diminuer s’il presse la touche PageDown.
public void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.PageUp))
{
sharpAmount += 1;
effect.Parameters["SharpAmount"].SetValue(sharpAmount);
}
else if (Keyboard.GetState().IsKeyDown(Keys.PageDown))
{
sharpAmount -= 1;
effect.Parameters["SharpAmount"].SetValue(sharpAmount);
}
}

Flouter une texture


Le flou (blur en anglais) s’obtient presque de la même manière que l’effet précédent : la
couleur du pixel est déterminée en fonction de la couleur des pixels voisins (figure 13-8),
sauf que cette fois ci vous n’utiliserez pas de seuil de netteté. Pour vous permettre de bien
visualiser l’effet, la distance séparant le pixel courant et les pixels voisins sera dynamique.
float Distance;
=Labat FM.book Page 317 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
317

float4 pixelShader(PixelInput input) : COLOR


{
float4 color = tex2D( TextureSampler,
float2(input.TexCoord.x+Distance, input.TexCoord.y+Distance));
color += tex2D( TextureSampler,
float2(input.TexCoord.x-Distance, input.TexCoord.y-Distance));
color += tex2D( TextureSampler,
float2(input.TexCoord.x+Distance, input.TexCoord.y-Distance));
color += tex2D( TextureSampler,
float2(input.TexCoord.x-Distance, input.TexCoord.y+Distance));
color = color / 4;
return( color );
}
Comme pour l’effet précédent, le paramètre dynamique sera modifié si le joueur appuie
sur la touche PageUp ou sur la touche PageDown.
float distance = 0;

public void Update(GameTime gameTime)


{
if (Keyboard.GetState().IsKeyDown(Keys.PageUp))
{
distance += 0.001f;
effect.Parameters["Distance"].SetValue(distance);
}
else if (Keyboard.GetState().IsKeyDown(Keys.PageDown))
{
distance -= 0.001f;
effect.Parameters["Distance"].SetValue(distance);
}
}
Vous remarquez que sur la figure suivante, la texture appliquée au cube est floutée.

Figure 13-8
Le cube flouté
=Labat FM.book Page 318 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


318

Modifier les couleurs d’une texture


Les pixels shaders suivants inversent les couleurs d’une texture.
Ici, il renvoie la couleur avec toutes ses composantes dans l’ordre classique.
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = tex2D(TextureSampler, input.TexCoord);
return (color.rgba);
}
Dans le pixel shader suivant, la composante rouge est inversée avec la composante verte.
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = tex2D( TextureSampler, input.TexCoord);
return( color.grba );
}
Il est également possible d’inverser le rouge et le bleu.
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = tex2D( TextureSampler, input.TexCoord);
return( color.bgra );
}
Un effet intéressant consiste à transformer la texture pour que ses couleurs varient
uniquement entre le noir et le blanc (on parle également de niveaux de gris). Votre premier
réflexe est peut-être de faire la moyenne des trois composantes. Cependant, l’œil humain
ne percevant pas de la même manière les trois composantes, vous devez leur appliquer
des coefficients. La Commission internationale de l’éclairage conseille d’utiliser les
coefficients suivants :
• 0,3 pour le rouge ;
• 0,59 pour le vert ;
• 0,11 pour le bleu.
• Pour convertir une texture en niveau de gris (comme sur la figure 13-9), vous pouvez
donc écrire le pixel shader suivant (la fonction dot fait le produit scalaire de deux
vecteurs).
float4 pixelShader(PixelInput input) : COLOR
{
float4 color = tex2D( TextureSampler, input.TexCoord);
color.rgb = dot(color.rgb, float3(0.3, 0.59, 0.11));
return( color );
}
Sur la figure suivante, la texture appliquée au cube apparaît en niveaux de gris.
=Labat FM.book Page 319 Vendredi, 19. juin 2009 4:01 16

Améliorer le rendu avec le High Level Shader Language


CHAPITRE 13
319

Figure 13-9
Une texture transformée en niveaux de gris

En résumé
Vous connaissez à présent tout ce qu’il faut pour intégrer des premiers effets à vos jeux,
à savoir :
• ce qu’est un shader ;
• la syntaxe du langage HLSL ;
• comment écrire un vertex shader et un pixel shader et comment les intégrer aux jeux.
=Labat FM.book Page 320 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 321 Vendredi, 19. juin 2009 4:01 16

A
Visual C# Express 2008

Si vous débutez en C#, ou si vous développez habituellement sous Linux, vous n’avez
peut-être encore jamais eu à utiliser un outil de la suite Visual Studio.
Cette annexe présente l’interface de Visual C# Express 2008, son éditeur de texte, ses
fonctionnalités pour vous faire gagner du temps lorsque vous développez et son débogueur.

Différencier solution et projet


Un projet regroupe un ou plusieurs fichiers et vise la création d’un assemblage (appelé
assembly en anglais). Un assemblage peut être un fichier .exe, mais aussi une DLL (pour
Dynamic Link Libraries, bibliothèque de liens dynamiques en français).
Les DLL (attention, il ne s’agit pas forcément de fichiers portant l’extension .dll) corres-
pondent aux projets de type Bibliothèque de classes (figure A-1), ou plus particulièrement
pour XNA, aux projets de type « Windows/Xbox 360/Zune Game Library » (figure A-2).
Une DLL ne comporte pas de point d’entrée comme la fonction Main, elle ne contient que
du code réutilisable par plusieurs applications.
Dans tout cet ouvrage, nous avons travaillé soit avec XNA ou bien en mode console.
Nous avons même utilisé le moteur physique FarseerPhysics, un projet créé par un autre
développeur et qui générait une DLL.
Une solution est automatiquement créée lorsque l’on crée un projet. Elle peut bien entendu
contenir plusieurs projets, comme lorsque vous écrivez une DLL parallèlement à un projet
exécutable et que la bibliothèque devra être utilisée dans ce projet. Dans ce cas, il est
conseillé de choisir l’option Créer le répertoire pour la solution, ce qui isole les différents
projets de la solution dans des sous-répertoires, rendant le tout beaucoup plus lisible.
=Labat FM.book Page 322 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


322

Figure A-1
Création d’une DLL .Net

Figure A-2
Création d’une DLL XNA
=Labat FM.book Page 323 Vendredi, 19. juin 2009 4:01 16

Visual C# Express 2008


ANNEXE A
323

Personnaliser l’interface
L’interface de Visual C# Express 2008 est composée de plusieurs fenêtres (l’explorateur
de solutions, l’explorateur de propriétés, etc.). Toutes ces fenêtres peuvent être remaniées
comme vous le désirez : vous pouvez les déplacer, les cacher ou encore en afficher d’autres.
Par exemple, amusez-vous à fermer toutes les fenêtres pour avoir un écran totalement
vierge. Allez ensuite dans le menu Affichage et sélection Explorateur de solution ou utili-
sez le raccourci clavier Ctrl + W, S. Cliquez avec le bouton droit sur la barre de titre de la
fenêtre qui s’est ouverte (figure A-3) : vous pouvez définir si elle doit être flottante ou ratta-
chable à un bord de la fenêtre (on parle également de fenêtre « ancrable »).

Figure A-3
Mode d’affichage de la fenêtre

Pour ancrer une fenêtre à un bord de la fenêtre, maintenez le clic sur sa barre de titre vers
la marque du bord désiré, elle s’y accroche automatiquement (figure A-4).

Figure A-4
La fonction d’ancrage
des fenêtres est très pratique

Vous avez la possibilité de n’afficher les fenêtres que lorsque votre souris passe sur le
bord de la fenêtre. Lorsque vous cliquez avec le bouton droit sur la barre de titre de la
fenêtre, choisissez l’option Masquer automatiquement.
Si vous disposez d’un écran assez grand et si vous aimez cette manière de travailler, vous
pouvez afficher deux fichiers (ou plus) en parallèle, aussi bien verticalement qu’horizon-
talement (figure A-5).
=Labat FM.book Page 324 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


324

Figure A-5
Avoir plusieurs fichiers ouverts en même temps peut-il améliorer la productivité ?

L’éditeur de texte
L’éditeur de texte de Visual C# Express 2008 est un outil particulièrement paramétrable. La
première chose que fait généralement un développeur après avoir installé l’IDE est de choisir
d’afficher le numéro des lignes (option qui n’est malheureusement pas activée par défaut).
1. Cliquez sur le menu Outils puis sur Options : vous accédez à la fenêtre des options
de l’éditeur de texte.
2. Dans l’arbre à gauche de la fenêtre, choisissez Éditeur de texte.
3. Sélectionnez C#.
4. Enfin, cochez la case Numéros de ligne (figure A-6).
Dans cette fenêtre, vous pouvez également définir la taille et le comportement des tabu-
lations, l’affichage des erreurs ou encore le comportement d’IntelliSense.

IntelliSense
IntelliSense est le composant de Microsoft qui assure la saisie automatique de code et qui, lorsque vous
écrivez un programme, vous propose une liste contextuelle d’éléments disponibles (propriétés, méthodes, etc.).
=Labat FM.book Page 325 Vendredi, 19. juin 2009 4:01 16

Visual C# Express 2008


ANNEXE A
325

Figure A-6
Les options de l’éditeur de texte

Les directives #region et #endregion ne sont pas spécifiques à la suite de Microsoft, mais
font partie intégrante du langage C#. Elles masquent une partie du code pour améliorer la
lisibilité.
#region Test
public class Class1
{
}
#endregion
Si vous copiez l’exemple ci-dessous dans l’éditeur de texte de Visual C# Express 2008,
vous verrez apparaîtrez un petit signe – à côté de l’expression #region. Lorsque vous
cliquez sur ce signe, le contenu est automatiquement masqué et remplacé par le mot Test.
Dans l’éditeur de texte, vous pouvez également choisir de masquer le contenu d’une
fonction, d’une classe, d’un espace de noms, etc., sans utiliser les régions (figure A-7).

Figure A-7
L’implémentation de la fonction est cachée
=Labat FM.book Page 326 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


326

Pour terminer avec l’éditeur de texte, lorsque vous ajoutez des instructions dans un
fichier, une barre verticale jaune apparaît à côté de ces nouvelles lignes (ou des lignes
modifiées). La barre deviendra verte une fois que vous aurez sauvegardé votre fichier.
Les barres disparaissent dès que le fichier est fermé (figure A-8).

Figure A-8
Changements de couleur de la barre verticale

Les extraits de code


Les extraits de code (appelé code snippet en anglais) servent à insérer rapidement du
code préconfiguré. L’extrait de code peut être une structure de base du langage (une
boucle for par exemple) ou un extrait de code plus complet que vous aurez défini.
Pour insérer un snippet :
1. Cliquez avec le bouton droit.
2. Sélectionnez Insérez un extrait…, ou bien utilisez le raccourci clavier Ctrl + K, X.
3. Choisissez ensuite l’extrait de code qui vous intéresse.
Sur la figure A-9, vous voyez l’ajout d’un extrait de code pour les propriétés. Vous
pouvez vous déplacer entre les différentes zones en couleur grâce à la touche Tab.

Figure A-9
Ajout d’un code snippet

Avec XNA, vous devez souvent réécrire toute un bloc de lignes concernant les directives
using… Pour gagner du temps, vous allez donc créer un code snippet qui évitera d’avoir
à réécrire toutes ces lignes.
L’extrait de code est représenté par un fichier XML qui doit porter l’extension .snippet. Le
schéma du fichier est disponible sur le site de MSDN à cette adresse : http://msdn.microsoft.com/
en-us/library/ms171418.aspx.
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
=Labat FM.book Page 327 Vendredi, 19. juin 2009 4:01 16

Visual C# Express 2008


ANNEXE A
327

<Header>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
<Title>Directives using pour XNA</Title>
<Shortcut>usingxna</Shortcut>
<Description>Insere automatiquement les directives using pour XNA.
➥ </Description>
<Author>Leonard LABAT</Author>
</Header>
<Snippet>
<Code Language="csharp"><![CDATA[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;
using Microsoft.Xna.Framework.Storage;]]></Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>

< ![CDATA[]]>
Le CDATA évite l’exécution d’une partie du fichier XML. Ce qui se situe dans cette balise ne sera donc pas
parsé.

Cliquez ensuite sur Outils, puis sur Gestionnaire des extraits de code, et enfin sur le
bouton Importer… Allez chercher le fichier .snippet. L’extrait de code est maintenant
prêt à être utilisé.

Refactoriser
Imaginez la situation suivante : vous vous êtes lancé dans la création d’une application
(un jeu, une application Windows, etc.) et vous êtes déjà bien avancé. Seulement, au bout
d’un moment, vous vous rendez compte que vous avez fait une erreur de design, ou vous
n’êtes pas satisfait d’un résultat : les modifications à apporter au projet risquent donc de
vous prendre beaucoup de temps.
La refactorisation (de l’anglais refactoring) permet de remanier facilement le code. Les
différentes possibilités de refactorisation sont accessibles depuis le menu Refactoriser
lorsque vous avez sélectionné un élément du code.
Si vous avez lu ce livre en entier, vous avez déjà rencontré une technique de refactorisation :
le renommage. En effet, par défaut dans tout projet XNA, la classe de base s’appelle
Game1. Si vous avez également créé une variable qui s’appelle Game1Int, et que finalement
=Labat FM.book Page 328 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


328

vous décidez de renommer la classe en MyGame, Visual C# Express 2008 procédera de


manière intelligente et ne renommera que la classe et ses utilisations.
Il existe plusieurs autres techniques :
• extraire la méthode, ce qui vous permettra de générer automatiquement une méthode à
partir d’instructions que vous aurez sélectionnées ;
• extraire l’interface, ce qui vous permettra de créer une interface à partir du nom d’une
fonction publique ;
• supprimer les paramètres, ce qui vous permettra de supprimer un argument de la
signature d’une fonction, etc.

Déboguer une application


Avant de déboguer un projet, vous devez vous assurer que vous le générez en mode Debug.
Pour cela, vérifiez que c’est bien Debug qui est sélectionné dans la liste Configuration de
solutions (figure A-10).

Figure A-10
Sélection de la configuration à appliquer

Attention
Avant de distribuer votre application, recompilez-la en mode Release. L’assemblage ainsi généré est
optimisé et ne comprend pas de données relatives au débogage.

Pour lancer une application et utiliser le débogueur, cliquez sur le menu Déboguer, puis
sur Démarrer le débogueur, ou utilisez le raccourci clavier F5. Pour stopper le débogage,
retournez dans le même menu et choisissez cette fois Arrêter le débogage, ou utilisez le
raccourci clavier Maj + F5.
Les points d’arrêt (break points en anglais) sont un élément essentiel du débogueur : vous
en placez sur chaque instruction où le programme doit suspendre son exécution. Ils sont
donc pratiques pour vérifier que le programme passe bien par certains endroits.
Pour placer un point d’arrêt sur une ligne, cliquez sur la colonne située à gauche de celle-ci
ou utilisez le raccourci F9. Le retrait d’un point d’arrêt se fait de la même manière. Lorsque
vous avez ajouté un point d’arrêt sur une ligne, celle-ci s’affiche en rouge dans l’éditeur
de texte.
=Labat FM.book Page 329 Vendredi, 19. juin 2009 4:01 16

Visual C# Express 2008


ANNEXE A
329

Placez un point d’arrêt quelque part dans le code (par exemple, dans la méthode Update() si
vous travaillez sur un jeu avec XNA) et lancez le débogueur. Dès que le programme atteint
le point d’arrêt, la fenêtre de Visual C# Express 2008 passe au premier plan. La ligne où s’est
arrêté le programme est maintenant visible en jaune dans l’éditeur de texte (figure A-11).

Figure A-11
L’exécution s’est suspendue sur le point d’arrêt

Vous pouvez afficher le contenu d’une variable simplement en passant dessus le curseur
de la souris. Pour les objets plus complexes, faites dérouler la liste des informations grâce
au signe + (figure A-12).

Figure A-12
Visualisation de la valeur d’une variable

Vous pouvez aussi voir le contenu des variables via les fenêtres espions. Pour ajouter un
espion sur une variable, faites un clic droit sur la variable et choisissez Ajouter un espion.
Son contenu est alors visible dans la fenêtre Espion (figure A-13). La fenêtre Variables
locales présente automatiquement les variables définies dans le contexte courant.

Figure A-13
Utilisation d’un espion
=Labat FM.book Page 330 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


330

Il ne s’agit ici que d’une présentation rapide du débogueur de Visual C# Express 2008. Il
possède bien d’autres fonctionnalités et le framework .Net dispose aussi des classes Debug
et Trace qui pourraient vous être utiles…

Raccourcis clavier utiles


La meilleure façon d’augmenter votre productivité est de maîtriser les différents raccourcis
clavier de Visual Studio. Le tableau A-1 énumère les plus couramment utilisés.

Toujours plus de raccourcis


Si vous avez envie d’abandonner complètement votre souris et de devenir un as du clavier, Microsoft a mis
à la disposition des développeurs C# un poster qui regroupe tous les raccourcis clavier disponibles dans
Visual Studio 2008.
http://www.microsoft.com/downloads/details.aspx?familyid=E5F902A8-5BB5-4CC6-907E-
472809749973&displaylang=en

Tableau A-1 Raccourcis clavier les plus couramment utilisés

Raccourci Description
Ctrl + M,O Masquer une zone.
Ctrl + M,M Déplier une zone masquée.
Ctrl + E,C Commenter la zone sélectionnée.
Ctrl + E,U Décommenter la zone sélectionnée.
TAB Insérer un snippet.
Shift + Alt + C Afficher la fenêtre d’ajout de classe.
Ctrl + J Afficher IntelliSense.
Ctrl + F4 Fermer le fichier ouvert.
Ctrl + Shift + B Générer le projet.
F5 Lancer le débogage.
=Labat FM.book Page 331 Vendredi, 19. juin 2009 4:01 16

B
Les bienfaits
de la documentation

Dans cette annexe, vous allez apprendre où trouver de la documentation et comment


générer la documentation de vos projets.

L’incontournable MSDN
La première chose à connaître lorsque vous débutez avec XNA ou, d’une manière plus
générale, avec la programmation .Net, c’est l’existence de MSDN (Microsoft Developer
Network). MSDN, c’est tout simplement :
• Un site web, qui est disponible en français (http://msdn.microsoft.com/fr-fr/) et sur lequel
vous retrouvez des actualités, des dossiers ou encore des événements à propos des
technologies Microsoft (figure B-1).
• Une gigantesque bibliothèque de documentations qui contient des tutoriaux et un
guide de référence sur toutes les classes du framework .Net ou XNA (figure B-2).
Cette base de connaissances est également disponible en téléchargement lorsque vous
installez un outil de la suite Visual Studio.
• Des forums de discussion où vous pourrez trouver de l’aide auprès des experts de la
communauté.
• Des blogs où les auteurs postent régulièrement leurs dernières trouvailles ou études, et
ce, pas uniquement sur des technologies Microsoft.
=Labat FM.book Page 332 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


332

Figure B-1
Le site web français de MSDN

Figure B-2
La documentation complète du framework est disponible
=Labat FM.book Page 333 Vendredi, 19. juin 2009 4:01 16

Les bienfaits de la documentation


ANNEXE B
333

MSDN propose un abonnement qui permet, entre autres, à ceux qui en bénéficient d’accéder
aux tout derniers logiciels de Microsoft en avant-première.
Enfin, MSDNAA (MSDN Academic Alliance) est un programme qui permet aux universités
et écoles adhérentes de proposer à leurs étudiants l’accès à divers logiciels Microsoft. Si
vous êtes étudiant et que votre structure enseignante vous le permet, vous avez par exemple
accès à un abonnement étudiant à l’XNA Creator Club pour tester vos jeux sur Xbox 360
(vous ne pourrez cependant pas vendre vos jeux).

Ressources sur le Web


La communauté XNA est grande, les blogs et les sites des gourous du framework sont très
nombreux et leur contenu est à la hauteur. Dans le tableau B-1, nous avons essayé de dresser
une courte liste de ces ressources, à visiter et explorer régulièrement en profondeur.

Tableau B-1 Sites et blogs incontournables

Site Langue Description


http://ziggyware.com/ En anglais Actuellement, presque deux cents ar ticles publiés.
Une véritable mine d’or.
http://blogs.msdn.com/shawnhar/ En anglais Le blog de Shawn Hargreaves.
http://msmvps.com/blogs/valentin/default.aspx En français Le blog de Valentin Billotte, MVP.
http://www.c2i.fr/ En français Le site web de Richard Clark, MVP.
http://blog.emmanueldeloget.com/ En français Le blog d’Emmanuel Deloget.
http://www.xnadevelopment.com/ En anglais Le site web de George Clingerman, MVP.
http://leonard-labat.blogspot.com/ En français Le blog de l’auteur de ce livre.

MVP
Le terme MVP (Microsoft Most Valuable Professional) est un titre décerné chaque année par Microsoft
à des professionnels des technologies Microsoft indépendants. Les experts qui reçoivent ce titre sont
récompensés pour le partage de leurs connaissances auprès d’autres membres des communautés
d’utilisateurs.

Figure B-3
Le logo généralement présent sur le blog
ou le site d’un MVP
=Labat FM.book Page 334 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


334

Générer de la documentation
Vous vous en doutez sûrement, apprendre à programmer sans support de documentation
est mission impossible. De la même manière, il est difficilement imaginable que vous
puissiez apprendre à vous servir d’une bibliothèque sans pouvoir vous appuyer sur une
source de documentation. Vous aurez donc certainement envie, vous aussi, d’utiliser les
fonctionnalités de génération de documentation de la suite Visual Studio afin de distribuer
vos projets accompagnés de cette source d’informations.
Nous l’avons vu au début du livre, il existe un format de commentaire particulier dédié à
la documentation, les trois slashs à la suite :
///

À la suite de ces slashs, vous écrivez une balise XML. Le tableau B-2 répertorie une
partie des balises existantes.

Tableau B-2 Les balises de documentation

Balise Description
<summary> Utilisée pour donner la description complète d’une classe, fonction, propriété ou variable.

<param> Utilisée pour la documentation d’un argument. Vous devez préciser le nom de l’argument.
<param name=”arg1”>Argument 1</param>
<returns> Utilisée pour documenter la valeur de retour d’une fonction.

<value> Utilisée pour documenter une propriété.

<paramref> Utilisée pour signaler qu’un mot, utilisé par exemple dans une balise <summary>, est un paramètre
de la fonction.
<summary>Je parle de <paramref name=”arg1” /> <summary>
<param name=”arg1”>Argument 1</param>
<c> Utilisée pour documenter du code sur une ligne (la différence avec du texte classique se situe au
niveau de l’affichage).
<code> Même chose que <c>, mais supporte plusieurs lignes.

<remarks> Utilisée pour ajouter des remarques ou commentaires par ticuliers.

<exception> Utilisée pour préciser le type d’exception que la fonction est susceptible de lever.

<exemple> Utilisée pour donner un exemple d’utilisation d’un élément.

<see> Utilisée pour créer un lien vers un autre élément.


<summary>Avez-vous vu <see cref=”method”>ma méthode</see> ?
<seealso> Utilisée pour créer un lien en bas de page de la documentation.

Une fois votre classe correctement documentée, signalez à l’environnement de dévelop-


pement qu’il doit générer un fichier .xml contenant la documentation du projet.
=Labat FM.book Page 335 Vendredi, 19. juin 2009 4:01 16

Les bienfaits de la documentation


ANNEXE B
335

1. Cliquez sur le menu Projet, puis sur Propriétés de …


2. Allez ensuite sur l’onglet Générer.
3. Cochez la case Fichier de documentation XML (figure B-4).

Figure B-4
Activation de la génération de documentation

4. Générez ensuite le projet et rendez-vous dans son répertoire de sortie : un fichier .xml
a bien été créé.

<?xml version="1.0" ?>


- <doc>
- <assembly>
  <name>ChapitreDix_2</name>
  </assembly>
- <members>
- <member name="M:ChapitreDix_2.Program.Main(System.String[])">
  <summary>The main entry point for the application.</summary>
  </member>
  </members>
  </doc>

Pour transformer votre documentation en une version plus lisible, essayez par exemple
l’utilitaire SandCastle (http://www.codeplex.com/Sandcastle) qui génère des fichiers HTML dans
le style MSDN (figure B-5).
=Labat FM.book Page 336 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


336

Figure B-5
Documentation générée avec SandCastle
=Labat FM.book Page 337 Vendredi, 19. juin 2009 4:01 16

Index

Numériques B C
3ds max 299 backface culling 271, 272 callback 154
Background 87 caméra 233, 265, 278
A balises 146 champ de vision 233
A* 186, 187 documentation 334 déplacer 277
abonnement 228 banque de sons 133 multijoueur 238
accesseur 179 BasicEffect 263, 264, 267, 298, Rectangle 233
accolade 16 304 carte
ActiveSongChanged 144 BeginShowKeyboardInput 156 bords 235
addition 9 BeginShowMessage 154 graphique 303
affichage 287 bibliothèque 29 case 190, 192
fil de fer 313 multimédia 142 coût 192
algorithme de classes 321 cast 180
A* 187 BinaryFormatter 180 casting 77
Blender 299 casual (jeu) 35
recherche de chemin 186
bloc catch 150
test 197, 204
catch 150 CDATA 327
alias, espace de noms 19
chaîne de caractères 8
alpha 216 finally 150
concaténer 24
ambient light 294 blur 316
champ de vision 265
AmbientLightColor 296 Body 224
char 7
animation 108 BodyFactory 224
classe 18, 20
sprite sheet 108 boîte de dialogue 154
Background 87
ApplyForce 224 booléen 6, 155 BasicEffect 263, 264, 298, 304
arrière-plan 86, 105 IsTrialMode 160 Body 224
aspectRatio 266 SimulateTrialMode 160 BodyFactory 224
assemblage 321 boucle 55 de test 110
assembly 321 do 55 Directory 169
Asteroids 207 for 56, 196 DrawableGameComponent 86
asynchrone 144 foreach 58, 196 droits d’accès 21
méthode 154 infinie 56 Exception 150
traitement 155 while 55, 196 File 172, 174
attribut brouillard 298 Game 75
Serializable 178 buffer 139, 172 Geom 227
speed 210 vider 175 Guide 152, 158
XmlIgnore 179 Buttons 72 hiérarchie 54
AudioEngine 136 ButtonState 71 KeyboardState 68
autocomplétion 32 byte 191 Map 191
=Labat FM.book Page 338 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


338

classe (suite) ContainsKey 62 documentation 334


MathHelper 266 conteneur de Références 165 balise 334
Matrix 291 Content données
MediaLibrary 142 Manager 37, 55, 136 extraire 253
MediaPlayer 142 HLSL 305 recevoir 253
mère 51 pipeline 29 récupérer 253
NetworkSession 251 Processor 144 dossier, jeu 168
Node 192 ContentImporter 181 dot 318
NodeList 195 continue 56 Draw 30, 40, 48, 99
partielle 22 coordonnées 259, 281 surcharge 40
Path 171 Copy 172 DrawableGameComponent 86, 128
PathFinding 196 couleur 279 DrawUserIndexedPrimitives 272
PhysicsSimulator 222 moduler 40 droits d’accès 21
Player 228 négatif 315
ServiceHelper 88 netteté 315 E
SignedInGamer 158 texture 318 échelle, modifier 291, 293
sprite 37, 45 Vector4 262 écran, partage 231
SpriteBatch 99, 304 Count 196 écrire dans un fichier 174, 175
statique 19 Create 172, 249 éditeur
Tile 190, 201 CreateLookAt 266 de cartes 162
WaveBank 138 CreatePerspectiveFieldOfView de texte 324
clavier 67, 75 265 effet 263
déplacement du sprite 69 CreateRectangleBody 224 fichier 310
récupérer l’état 68 CreateRectangleGeom 227 flou 316
Clear 214, 244 CreateRotationY 291 négatif 315
Clingerman, George 105, 108 CreateScale 291 netteté 315
Close 175 CreateTranslation 293 niveau de gris 318
code snippet 326 culling 313 prise en charge de la lumière
CodePlex 219 CullMode 271 296
collection 58 CurrentTechnique 312 technique 264
Components 87 vague 314
générique 58 D else 12
List 194, 211 déboguer 328 else if 13
ServiceHelper 201 délégué 228 encapsulation 21
collision 200, 213 Delete 172 énumération 68
détection 91 démo 160 Buttons 72
pixel 215 désérialisation 148 ButtonState 71
Player 228 Deserialize 180 CullMode 271
rectangle 213 détection, collision 91 Keys 68
CollisionPerPixels 216 dictionary 59 PlayerIndex 71, 238
Color 31, 112, 279 dictionnaire 62 PrimitiveType 261
Combine 171 diffuse lighting 294 SpriteEffects 116
commentaire 11 Dijkstra 186 TileType 192
Components 87 directive, using 68, 326 espace
compression 144 Directory 19, 169 de noms 19, 169, 178, 179
condition 11, 56 DirectX 29 déclaration 68
configuration utilisateur 146 Dispose 253 Microsoft.Xna.Fra-
const 8 distance de Manhattan 188, 192 mework.Input 68
constante 8 division 9 de stockage 145
constructeur 19, 22, 51 par zéro 149 dossier de l’utilisateur 146
argument 23 DLL 321 dossier du jeu 145
Vector2 39 do, boucle 55 espion 329
=Labat FM.book Page 339 Vendredi, 19. juin 2009 4:01 16

Index
339

état, souris 201 framework 29 IMouseService 200


événement 228 FX Composer 305 incrémentation 10
ActiveSongChanged 144 index 271
exception 148, 150 G index buffer 271, 272
personnaliser 151 gâchette analogique 72 inertie 227
exe 321 Game 75 Initialize 30, 41, 42, 210, 211
expédition, méthode 253 GamePadState 71 InitializeVertices 279, 295
extrait de code 326 gameplay 80 insertion dichotomique 194
Gamer 158 instancier, constructeur 22
F gamer instruction continue 56
factoriser le code 14 services 249 intelligence artificielle 185
far plane 264 tag 249 IntelliSense 32, 324
FarseerPhysics 207, 219 Gamer Services 152 interface 74
fenêtre 208 GamerCard, afficher 158 extraire 328
feuille de sprites 109 GamerServicesNotAvailableExce IMouseService 200
fichier ption 148 règle de nommage 74
écrire 174, 175 GameThumbnail 61 IsConnected 71
lire 177 GameTime 88 IsDataAvailable 253
manipuler 172 Geom 227 IsTrialMode 160
sérialiser 178 gestionnaire
d’exceptions 148 J
field of view 265
fil de fer 313 de contenu 125, 144 jeu
File 19, 172, 174 image 55, 60 de rôle 50
finally 150 get 22 en réseau 248
Find 251 GetData 215 Mario 218
float 116 GetLength 57 plates-formes 218
floatLxC 307 GetPressedKeys 69 tile-based 37
flou 316 GLSL 304 Join 251
Flush 175 GPU 304 joueur connecté 158
flux 174, 175, 176 graphics 32, 41, 208
FogEnabled 298 GraphicsDevice 244, 267 K
fonction 15, 17 gravité 222 Keyboard 68
BeginShowKeyboardInput 156 GUI 80, 114 KeyboardState 68
CollisionPerPixels 216 wWinForms 81 KeyHasBeenPressed 232
définir 17 XNA Simple Gui 81 Keys 68
dot 318 Guide 152, 158 Keys.Escape 69
mathématique 224
override 51 H L
ResetPosition 229 Half Life 2 217 layerDepth 116
SetVibration 72 Hargreaves, Shawn 128 lecteur réseau 146
for, boucle 56, 196 Havok 217 licence 219
force 224 héritage 51, 52 lire un fichier 177
foreach, boucle 58, 196 multiple 50 List 211
format HLSL (High Level Shader liste 194
CSV 147 Language) 304 fermée 187
INI 148 générique 195
mp3 133 I ouverte 187, 194
wav 133 IAsyncResult 155 triée 194
XML 146 if 11 vider 214
FPS (Frames Per Second) 128 image, gestionnaire 55, 60 Live, compte 248
frame 46 Imagine Cup 29 Load 299
=Labat FM.book Page 340 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


340

LoadContent 30, 48, 61, 211, 272 DrawUserIndexedPrimitives MSDN (Microsoft Developer
lumière 294 272 Network) 331
d’ambiance 294, 296, 312 extraire 328 MSDNAA (MSDN Academic
diffuse 294, 296 Flush 175 Alliance) 333
directionnelle 296 générique 77 msElapsed 203
spéculaire 294, 297 GetData 215 multijoueur 237
GetLength 57 multiplication 9
M GetPressedKeys 69
Main 15 Initialize 30, 211 N
manette 67, 71, 238 InitializeVertices 295 near plane 264
gâchette analogique 72 Join 251 netteté 315
multijoueur 71 KeyHasBeenPressed 232 NetworkSession 249, 251, 253
pad directionnel 72 Load 299 NetworkSessionProperties 251
stick analogique 72 LoadContent 30, 48, 61, 211 Next 210
vibration 72 Next 210 nextColor 112
Manhattan 192 Open 174 niveau 187
Map 191 Play 144 de gris 318
Mario 218 PlayCue 136 Node 192
Math 19, 20 privée, Initialize 210 NodeList 195
MathHelper 266 ReadType 253 nœud 187, 192
matrice 263 ReceiveData 253 coût de passage 189, 200
contenu 307 ResetPosition 210 nombre
de rotation 291 SendData 253 aléatoire 210
de vue 266 ShowGamerCard 158 entier 4
définir 306 statique 169 réel 6
résultat 266 Find 251 notation pointée 307
Matrix 263, 291, 307 synchrone 251
MediaLibrary 142 UnloadContent 30 O
MediaPlayer 142, 144 Update 30, 42, 48, 71, 208, objet 18
mémoire 3 209, 211, 214, 278, 291 AudioEngine 136
mesh 299 ValidCoordinates 191 BinaryFormatter 180
méthode Write 175 ContentManager 62
ApplyForce 224 WriteLine 175 Cue 137
asynchrone 154, 155, 228 Microsoft.XNA.Framework.Gam GamePadState 71
BeginShowMessage 154 e.dll 29 Gamer 158
Clear 214, 244 mise à l’échelle 263 GameTime 88
Close 175 mode graphics 41, 208
Combine 171 démonstration 160 Keyboard 68
Copy 172 Release 149 MouseState 70
Create 172, 249 modèle 299 random 210
CreateLookAt 266 ajouter au projet 299 SoundBank 136
CreatePerspectiveFieldOfView charger 299 SpriteBatch 128
265 modeleur 3D 299 SpriteFont 128
CreateRectangleBody 224 molette 70, 71 Spritefont 125
CreateRectangleGeom 227 mot-clé StorageContainer 153, 168
CreateRotationY 291 override 51 StreamWriter 175
CreateTranslation 293 throw 151 String 19
Delete 172 moteur TimeSpan 211
Deserialize 180 graphique 29 Vector2 222
Dispose 253 physique 207, 217 WaveBank 136
Draw 30, 40, 48, 99 MouseState 70 Open 174
=Labat FM.book Page 341 Vendredi, 19. juin 2009 4:01 16

Index
341

OpenGL 304 preset 140 Role Playing Game 50


opérateur primitive 261 rotation 116, 263
&& 216 type 261 matrice 291
conditionnel 12 PrimitiveType 261 point d’origine 118
incrémentation 10 private 22
logique 14 procédure 15 S
ternaire 214 déclarer 16 SandCastle 335
override 51 nom 16 sauvegarde, StorageContainer 153
processus asynchrone 251 SavedGames 146
P projection scrolling 105, 232
PacketReader 253 en perspective 264 classe de test 106
PacketWriter 253 orthogonale 265 ScrollWheelValue 71
pad directionnel 72 projet 321 sémantique 308
paquet 249 partager 29 SendData 253
paramètre planifier 83 séparation du code 30
callback 154 Pong 83 sérialisation 148
PlayerIndex 158 pseudo-code 84 sérialiser 178
supprimer 328 sonore, créer 132 en binaire 178
partial 22 propriété un fichier 178
passe 264, 310 AmbientLightColor 296 XML 179
Path 171 Count 196 Serializable 178
Pathfinding 186, 196 FogEnabled 298 service
performances 63, 253, 266 Game 87 état de la souris 201
temps d’exécution 63 IsConnected 71 XNA 73, 75
IsDataAvailable 253 ServiceHelper 88, 201
utilisation mémoire 63
ScrollWheelValue 71 session 249
périphérique 67, 154
Services 75 set 22
clavier 67
SpecularColor 297 SetVibration 72
manette 67, 71
Techniques 312 shader 303
souris 70, 200, 201
ShowGamerCard 158
spécialisé 73 TitleLocation 168
SignedInGamer 158
batterie 73 pseudo-code 84
SimulateTrialMode 160
guitare 73 public 22
Socket 19
tapis de danse 73 solo 232
perspective, projection 264 R
solution 321
Phun 218 raccourcis clavier 330 son
PhysicsSimulator 222 random 210 API
physique 217 rastérisation 304, 308 SoundEffect 141
piste, Cue 136 ReadType 253 XACT 131
pixel shader 304, 308, 310 ReceiveData 253 bonnes pratiques 144
plan rectangle 214 compression 139
éloigné 264 caméra 233 ininterrompu 139
proche 264 refactoriser 327 lire 136, 139, 141
PlayCue 136 référence 93 streaming 138
Player 228 Release 149 SoundBank 136
PlayerIndex 71, 158, 238 rendu, shader 303 SoundEffect 141
plein écran 33 renommage 327 lire
point d’arrêt 328 repère, main 260 musique 142
police de caractères 126 réseau 248 son 141
Pong 83, 231 quitter une partie 253 MediaPlayer (classe) 142
Portal 217 ResetPosition 210, 229 Zune 141
Position 281 réverbération 140 sourceRectangle 213
=Labat FM.book Page 342 Vendredi, 19. juin 2009 4:01 16

Développement XNA pour la XBox et le PC


342

souris 70 System.Diagnostics 64 TextureManager 62


curseur 71 System.IO 169 texturer 99, 285
molette 71 System.Runtime.Serialization.For this 23
soustraction 9 matters.Binary 178 throw 151
specular light 294 System.XML.Serialization 179 Tile 190, 201
SpecularColor 297 TileType 192
T TimeSpan 19, 211
speed 210
splitté 231 tableau 57 TitleLocation 168
sprite 36, 40 byte 191, 235 ToRadians 266
afficher 37, 47 Color 215 transformation 291
animation 108 de tableaux 307 translation 263, 293
classe 37, 45 multi-dimensionnel 57 trial mode 160
déplacement au clavier 69 parcourir 57, 58 TrueSpace 299
échelle 119 tileList 233 try … catch 158
inversion 120 Tales of Phantasia 37
technique 310 U
mouvement 41
ordre d’affichage 124 effet 264 UnloadContent 30
passe 264 Update 30, 42, 48, 71, 208, 209,
profondeur d’affichage 116,
124 Techniques 312 211, 214, 278, 291
rotation 116 test 11, 197, 249 using 19, 326
sheet 108 combiner 14
condition 13 V
sprite sheet 108
test.sav 175 ValidCoordinates 191
transformation 116
Tetris 35 variable 4
SpriteBatch 86, 99, 128, 304
texte aspectRatio 266
SpriteEffects 116
FPS (Frames Per Second) 128 sourceRectangle 213
SpriteFont 128
optimisation 129 statique 209
Spritefont 125
police de caractères 126 vecteur 39, 41, 262
starter kit 26
Spritefont 125 composant 307
statique texture 46, 281 Vector2 39, 46, 116, 222, 262
méthode 265, 266 à cheval 291 constructeur 39
variable 209 appliquer 100 Vector3 262, 273
stick analogique 72 arrière-plan 105 vertex 260
Stopwatch 64 associer à une couleur 284 couleur 279
StorageContainer 153, 168 coordonnées 281 ordre d’affichage 271, 272
STR 190 couleur 318 shader 304, 309
stratégie en temps réel 190 différente selon face 289 VertexDeclaration 282
streaming 138 flouter 316 VertexOutput 309
StreamWriter 175 n’afficher qu’une partie 283 VertexPositionColor 267, 279
string 8 négatif 315 VertexPositionColorTexture 284
structure 31, 308 netteté 315 VertexPositionNormalTexture 295
de contrôle 308 portion 102 VertexPositionTexture 281
Vector2 39 problème 287 vibration de la manette 72
VertexPositionNormalTexture redimensionner 100 Viewport 232, 237, 241
295 répéter 283 Visual Studio 321
surcharge 39, 99 scrolling 105 Viterbi 186
switch 242 teinte, varier 112 vitesse, déplacement du
swizzling 307 TextureCoordinate 281 personnage 203
synchrone 154 TextureEnabled 282 void 16, 17
=Labat FM.book Page 343 Vendredi, 19. juin 2009 4:01 16

Index
343

vue 244 WriteLine 175 Xbox,


affichage classique 246 wWinForms 81 BeginShowStorageDeviceSele
dessiner 238 ctor 153
matrice 266 X Xbox 360
nettoyer le contenu 244 XACT 131 transfert de jeu 33
réutiliser 248 compression 139 XML (eXtensible Markup
ADPCM (Windows) 139 Language) 146, 334
W XMA (Xbox 360) 139 désérialiser 181
Cue 133 sérialiser 179
WaveBank 136, 138
formats supportés 133 XmlIgnore 179
buffer 139
réverbération 140
offset 139 XNA Simple Gui 81
Sound Bank 133
while 196 XNB 38
streaming 138
while (boucle) 55 Wave 133 xnb 181
Wii Sport 35 Wave Bank 133
Windows Game Library 162 Z
XACT Auditioning Utility 135
Windows Media Player 142 Xbox LIVE 152 Zelda 37
Write 175 Xbox Live 29 Zune 27, 132, 146
=Labat FM.book Page 344 Vendredi, 19. juin 2009 4:01 16
L. Labat
Développement XNA Développement
pour la Xbox et le PC
XNA

XNA pour la Xbox et le PC


L. Labat
Grâce au tandem Live et XNA, la programmation de jeu vidéo pour PC et Xbox 360 est accessible au plus Passionné par le
grand nombre : il n’est plus nécessaire d’investir dans de ruineux outils pour donner libre cours à ses développement et les jeux
idées de jeux et les réaliser. Cet ouvrage permettra au lecteur de s’approprier le framework XNA 3.0, vidéo, Léonard Labat assure
une veille sur les technologies
mais également de comprendre comment s’organise un projet de développement de jeu vidéo. Microsoft en publiant
régulièrement sur son blog
Accéder aux dernières technologies de développement PC (http://leonard-labat.blogspot.
et Xbox 360 avec le framework XNA 3.0 com/). Il évolue au sein du
Pour accompagner l’explosion du développement amateur favorisé par la plate-forme de distribution en ligne Live,
Microsoft a mis au point le framework XNA pour fournir toutes les briques nécessaires à la création de jeu vidéo.
Supports de référence du Live, Xbox 360 et PC sont, grâce à XNA, les deux plates-formes les plus propices pour
laboratoire des technologies
.Net de SUPINFO
(http://www.labo-dotnet.
com/).
pour la Xbox et le PC
les studios indépendants, les freelances et les particuliers qui souhaitent faire connaître, voire commercialiser,
leurs réalisations.

Un manuel complet pour se lancer dans un projet de création


de jeu vidéo Premiers pas en développement de jeu vidéo
Ce livre accompagne le lecteur, débutant ou non, dans la conduite d’un projet de jeu en C#, qu’il s’agisse de pro-
grammer des événements, de créer un environnement sonore, ou de choisir ses moteurs graphique et physique et
de les exploiter. L’auteur y détaille les techniques de programmation 2D et 3D. Il explore également les techniques
graphiques et sonores avancées (effets, textures, défilement, transformations, animation, éclairage, design sonore,
streaming) mais aussi certains algorithmes d’intelligence artificielle, sans oublier l’inclusion du mode multijoueur en
réseau ou en écran splitté.

Au sommaire
XNA et son environnement • Débuter en C# • Types de données • Commenter le code • Conditions • Fonctions et pro-
cédures • Classes et espace de noms • Prise en main • EDI • Starter kit • Architecture d’un projet XNA • Créer un
projet • Outils pour la Xbox 360 • Les sprites • Afficher plusieurs sprites • La classe Sprite • Gestionnaire d’images :
boucles, tableaux et collections • Mesure des performances • Interaction avec le joueur • Périphériques • Services • GUI
• Programmer un Pong • Pseudo-code • Création du projet • Arrière-plan, raquette, balle • Améliorer le jeu • Textures,
défilement, animation • Texturer un rectangle • Scrolling • Sprites sheets • Variation de teinte • Transformations •
Spritefont • Sonorisation • XACT et SoundEffect • Créer un projet sonore • Lire un son et un morceau de musique •
Streaming • Design sonore • Exceptions et gestion des fichiers : sauvegarder et charger un niveau • Espace de stockage •
Sérialisation • Exceptions • Gamer Services • Un éditeur de cartes • Content Importers • Version démo • Pathfinding :
programmer le déplacement des personnages • Algorithme et intelligence artificielle • Implémenter l’algorithme A* • Colli-
Léonard Labat

Développement
sions et physique • Zone de collision • Moteur physique • Mode multijoueur • Partager l’écran • Gestion des caméras
• En réseau avec Live • Programmation 3D • Coordonnées, primitives, vertices, vecteurs, matrices, transformations,
effets, projection • Caméras • Matrices de vue et de projection • Appliquer une couleur à un vertex • Plaquer une
texture • Transformations des objets • Lumières • Éclairer la scène • Exploiter les modèles • Améliorer le rendu avec
le High Level Shader Language • Vertex shaders et pixel shaders • Syntaxe du HLSL • Fichier d’effet • Ondulation •
Textures : en négatif, netteté, flou, couleur • Annexes • Visual C# Express 2008 • La documentation.

Conception : Nord Compo


À qui s’adresse cet ouvrage ?
– Aux étudiants en programmation qui désirent adapter leurs connaissances aux spécificités du développement de
jeu pour PC et Xbox.
– Aux studios indépendants et freelances qui souhaitent passer à XNA.
– À l’amateur curieux qui a choisi XNA pour développer son premier jeu.

Vous aimerez peut-être aussi