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.
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
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
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.
• 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.
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
Version
XNA 3.0 est disponible depuis le 30 octobre 2008, c’est sur cette version que ce livre se focalise.
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.
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
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
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
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
CHAPITRE 5
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
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
CHAPITRE 11
CHAPITRE 12
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
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#.
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
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.
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
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.
Débuter en C#
CHAPITRE 1
5
Figure 1-3
La compilation du programme a échoué
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;
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;
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é !
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;
Débuter en C#
CHAPITRE 1
9
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
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 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++;
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
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
/*
* Commentaire sur plusieurs lignes
*/
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 :
Opérateur Description
== Test d’égalité
!= Test de différence
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;
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
Figure 1-6
Vos programmes se compliquent…
Vous pouvez combiner plusieurs tests à la fois grâce aux 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
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
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();
}
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.
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
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.
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
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));
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();
}
}
}
Débuter en C#
CHAPITRE 1
21
Figure 1-8
Création d’une nouvelle classe
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.
Droit Description
Public Accessible depuis l’extérieur de l’objet
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
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
Figure 2-1
Téléchargement de Microsoft
Visual C# Express 2008
=Labat FM.book Page 26 Vendredi, 19. juin 2009 4:01 16
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
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
ê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.
Figure 2-4
Le catalogue de jeux disponible sur le site de la communauté
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).
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
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
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
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
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";
}
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
Figure 3-1
En 1972, Pong est le
premier gros succès du jeu
vidéo
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
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
Figure 3-4
Ajout d’un objet au Content Manager
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
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
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
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.
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
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
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
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
base.Update(gameTime);
}
spriteBatch.Begin();
spriteBatch.Draw(balleTexture, ballePosition, Color.White);
spriteBatch.Draw(balleTexture, ballePosition2, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
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
{
Vector2 position;
Texture2D texture;
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
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
personnage.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
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
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
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
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
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
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
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.
0
1
2
3
4
0
2
=Labat FM.book Page 57 Vendredi, 19. juin 2009 4:01 16
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.
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
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
Attention
Dans une collection de ce type, chaque clé doit être unique.
=Labat FM.book Page 60 Vendredi, 19. juin 2009 4:01 16
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.
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
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
Pour terminer, dans la classe Game1, déclarez votre TextureManager, initialisez-le et modifiez
l’appel à LoadContent() de vos sprites.
TextureManager textureManager;
Figure 3-13
Utilisation mémoire (les deux courbes se chevauchent)
=Labat FM.book Page 64 Vendredi, 19. juin 2009 4:01 16
stopWatch.Start();
foreach (Sprite sprite in sprites)
sprite.LoadContent(textureManager, "GameThumbnail");
stopWatch.Stop();
}
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
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.
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
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
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
Figure 4-2
Le joueur peut maintenant déplacer son sprite où bon lui semble.
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
À 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();
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;
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.
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();
}
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
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 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 :
Contrainte Description
where T : struct Le type T doit être un type valeur.
where T : new() Le type T doit disposer d’un constructeur public sans paramètres.
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;
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;
À 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);
}
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.
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
Figure 5-1
Diagramme des classes bat et ball
=Labat FM.book Page 85 Vendredi, 19. juin 2009 4:01 16
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
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)
{
}
SpriteBatch spriteBatch;
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
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;
}
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
Components.Add(new Background(this));
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
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;
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
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));
started = false;
}
base.Initialize();
}
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 !";
}
}
}
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
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 ?
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.
Figure 6-1
Votre projet devrait ressembler
à ceci
Figure 6-2
Un sprite affiché simple-
ment avec la classe Sprite
actuelle
Figure 6-3
Le détail de la première surcharge
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.
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
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
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
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
Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}
Figure 6-9
Le jeu Super Tux utilise le scrolling
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
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;
}
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
=Labat FM.book Page 108 Vendredi, 19. juin 2009 4:01 16
Figure 6-11
Le début d’un clone de Super Mario
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
Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}
float index = 0;
int maxIndex = 0;
this.sourceRectangle = sourceRectangle;
}
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;
}
base.Update(gameTime);
}
spriteBatch.Begin();
sprite.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
=Labat FM.book Page 112 Vendredi, 19. juin 2009 4:01 16
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
Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}
if (color == Color.White)
nextColor = Color.Gray;
else if (color == Color.Gray)
nextColor = Color.White;
}
Texture2D texture;
public Texture2D Texture
{
get { return texture; }
}
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
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
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; }
}
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; }
}
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; }
}
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
{
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
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);
}
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
À 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é
Figure 6-19
Il existe une méthode qui permet d’afficher simplement du texte
<Style>Regular</Style>
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</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
<FontName>Verdana</FontName>
<Size>26</Size>
<Spacing>10</Spacing>
<UseKerning>true</UseKerning>
<Style>Bold</Style>
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</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
spriteBatch.Begin();
spriteBatch.DrawString(font, "test", new Vector2(0, 0), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
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
<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.
Zune
L’utilisation de XACT est impossible pour un projet de jeu Zune.
Figure 7-1
L’interface de XACT
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.
Figure 7-2
Un fichier de configuration
par plate-forme
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
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.
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
La sonorisation
CHAPITRE 7
139
Figure 7-8
Le nouveau constructeur de la classe WaveBank
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
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
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.
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
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
La sonorisation
CHAPITRE 7
143
SpriteBatch spriteBatch;
MediaLibrary sampleMediaLibrary;
Random rand;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
base.Update(gameTime);
}
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.
Zune
Sur le Zune, cet espace de stockage se limite à 16 Mo. Cela correspond à la mémoire vive qu’un jeu peut
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
• 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
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
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
int a = 10;
int b = 0;
try
{
a /= b;
}
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
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;
}
}
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
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));
}
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.
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.
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
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));
}
}
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
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 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.
Figure 8-7
Le joueur peut aisément écrire un message dans cette boîte de dialogue
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
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));
}
base.Update(gameTime);
}
Full Mode
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}
base.Update(gameTime);
}
Trial Mode
=Labat FM.book Page 162 Vendredi, 19. juin 2009 4:01 16
public Tile()
{
}
public Map()
{
}
keyboardState = Keyboard.GetState();
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;
public Chapitre_8()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferHeight = 160;
graphics.PreferredBackBufferWidth = 160;
}
base.Update(gameTime);
}
base.Draw(gameTime);
}
}
Figure 8-10
Le chemin complet vers le dossier du jeu
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
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));
}
else if (Keyboard.GetState().IsKeyDown(Keys.D))
{
try
{
=Labat FM.book Page 171 Vendredi, 19. juin 2009 4:01 16
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);
}
base.Draw(gameTime);
}
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.
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
public ChapitreHuit()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.Components.Add(new GamerServicesComponent(this));
}
base.Draw(gameTime);
}
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
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 :
catch (Exception e)
{
Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new
➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new
➥ AsyncCallback(EndShowMessageBox), null);
}
}
base.Update(gameTime);
}
base.Draw(gameTime);
}
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
base.Draw(gameTime);
}
=Labat FM.book Page 178 Vendredi, 19. juin 2009 4:01 16
[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
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);
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
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);
Figure 8-12
Le fichier XML représentant
la carte
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);
map.LoadContent(Content);
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
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;
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.
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.
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
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é
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
Figure 9-3
… jusqu’au nœud
de destination
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
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
Sinon
// On a trouvé le bon endroit pour l’insertion
Gauche = Centre
Arrêter l’algorithme
Fin Si
FinTantQue
Figure 9-5
Insertion dichotomique du
chiffre 3 dans une liste
openList.Add(startNode);
if (current.Tile == endTile)
{
=Labat FM.book Page 197 Vendredi, 19. juin 2009 4:01 16
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
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
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
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
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.
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
Tree = 1,
Water = 2,
Human = -1,
};
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;
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
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;
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
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
Map map;
Hero heros;
public ChapitreNeuf()
{
graphics = new GraphicsDeviceManager(this);
ServiceHelper.Game = this;
Components.Add(new MouseService(this));
Content.RootDirectory = "Content";
{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}
});
base.Initialize();
}
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
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.
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.
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
• 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.
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.Y < 0)
Position = new Vector2(Position.X, 0);
À 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
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);
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
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));
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Player playerShip;
List<Asteroid> asteroids = new List<Asteroid>();
public ChapitreDix()
{
=Labat FM.book Page 212 Vendredi, 19. juin 2009 4:01 16
base.Initialize();
}
playerShip.LoadContent(Content, "ship");
playerShip.ResetPosition();
}
playerShip.Update(gameTime);
elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime;
base.Update(gameTime);
}
spriteBatch.Begin();
playerShip.Draw(spriteBatch);
foreach (Asteroid asteroid in asteroids)
=Labat FM.book Page 213 Vendredi, 19. juin 2009 4:01 16
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
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
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
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
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.
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
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
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
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
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.
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;
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));
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
Figure 10-13
La nouvelle version du
jeu… sans les collisions !
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é.