Développement
XNA pour la Xbox et le PC Premiers pas en développement de jeu vidéo
Léonard Labat
Développement
XNA pour la Xbox et le PC
Chez le même éditeur Dans la thématique du jeu vidéo
Freemind – Boostez votre efficacité. X. Delengaigne, P. Mongin. N°12448, 2009, 272 pages.
RPG Maker. Créez votre gameplay et déployez votre jeu de rôle. S. Ronce. – N°12562, à paraître.
Spip 2 – Premiers pas pour créer son site avec Spip 2.0.3. A.-L. Quatravaux, D. Quatravaux. – N°12502, 2009, 300 pages.
Équideow. Le guide du bon éleveur. Perline et L. Noisette. – N°12521, à paraître.
Réussir son site web avec XHTML et CSS. M. Nebra. – N°12307, 2e édition, 2008, 306 pages.
Dans la même collection ActionScript 3. Programmation séquentielle et orientée objet. D. Tardivau. – N°12552, 2e édition, 2009,448 pages. PHP/MySQL avec Dreamweaver CS4. Les clés pour réussir son site marchand. J.-M. Defrance. – N°12551, 2009, 548 pages. Sécurité PHP 5 et MySQL. D. Seguy et P. Gamache. – N°12554, 2009, 284 pages. Sécurité informatique. Principes et méthode à l’usage des DSI, RSSI et administrateurs. L. Bloch, C. Wolfhugel. – N°12525, 2009, 292 pages. Programmation OpenOffice.org 3. Macros, OOoBASIC et API. B. Marcelly et L. Godard. – N°12522, 2009, 920 pages. Dreamweaver CS4 Styles CSS. Composants Spry-XM, comportements JavaScrip, comportements serveur PHP-MySQL. T. Audoux et J.-M. Defrance. – N°12462, 2009, 620 pages. Programmation Python. Conception et optimisation. T. Ziadé. – N°12483, 2e édition, 2009, 586 pages. CSS2. Pratique. du design web. R. Goetter. – N°12461, 3e édition, 2009, 318 pages. Programmation Flex 3. Applications Internet riches avec Flash ActionScript 3, MXML et Flex Builder. A. Vannieuwenhuyze. – N°12387, 2008, 430 pages.
Réussir un site web d’association… avec des outils libres ! A.-L. Quatravaux et D. Quatravaux. – N°12000, 2e édition, 2007, 372 pages. Réussir son site e-commerce avec osCommerce. D. Mercer. – N°11932, 2007, 446 pages. Open ERP – Pour une gestion d’entreprise efficace et intégrée. F. Pinckaers, G. Gardiner. – N°12261, 2008, 276 pages. PGP/GPG – Assurer la confidentialité de ses mails et fichiers. M. Lucas, ad. par D. Garance , contrib. J.-M. Thomas. – N°12001, 2006, 248 pages. Mozilla Thunderbird – Le mail sûr et sans spam. D. Garance, A.-L. et D. Quatravaux. – N°11609, 2005, 300 pages avec CD-Rom. Firefox. Retrouvez votre efficacité sur le Web ! T. Trubacz, préface de T. Nitot. – N°11604, 2005, 250 pages. Hackez votre Eee PC – L’ultraportable efficace. C. Guelff. – N°12437, 2009, 306 pages. Monter son serveur de mails Postfix sous Linux. M. Bäck et al., adapté par P. Tonnerre. – N°11931, 2006, 360 pages. Ergonomie web – Pour des sites web efficaces. A. Boucher. – N°12479, 2e édition 2009, 440 pages.
WPF par la pratique. T. Lebrun. – N°12422, 2008, 318 pages.
Joomla et VirtueMart – Réussir sa boutique en ligne. V. Isaksen, avec la contribution de T. Tardif. – N°12381, 2008, 306 pages.
PHP 5 avancé. E. Daspet et P. Pierre de Geyer. – N°12369, 5e édition, 2008, 884 pages.
La 3D libre avec Blender. O. Saraja. – N°12385, 3e édition, 2008, 456 pages avec DVD-Rom.
Bien développer pour le Web 2.0. Bonnes pratiques Ajax Prototype, Script.aculo.us, accessibilité, JavaScript, DOM, XHTML/CSS. C. Porteneuve. – N°12391, 2e édition, 2008, 674 pages.
Dessiner ses plans avec QCad – Le DAO pour tous. A. Pascual – N°12397, 2009, 278 pages. Inkscape efficace. C. Gémy – N°12425, 2009, 280 pages.
Dans la collection « Accès Libre »
Ubuntu efficace. L. Dricot. – N°12362, 3e édition, à paraître 2009.
Linux aux petits oignons. K. Novak. – N°12424, 2009, 546 pages.
Gimp 2.6 – Débuter en retouche photo et graphisme libre. D. Robert. – N°12480, 4e édition, 2009, 350 pages.
Inkscape. Premiers pas en dessin vectoriel. N. Dufour, collab. E. de Castro Guerra. – N°12444, 2009, 376 pages.
Gimp 2.4 efficace – Dessin et retouche photo. C. Gémy. – N°12152, 2008, 402 pages avec CD-Rom.
MediaWiki efficace. D. Barrett. – N°12466, 2009, 372 pages. Économie du logiciel libre. F. Elie. – N°12463, 2009, 195 pages.
Dotclear 2 – Créer et administrer son blog. A. Caillau. – N°12407, 2008, 242 pages.
Développement
XNA pour la Xbox et le PC Premiers pas en développement de jeu vidéo
Léonard Labat
ÉDITIONS EYROLLES 61, bd Saint-Germain 75240 Paris Cedex 05 www.editions-eyrolles.com
Le code de la propriété intellectuelle du 1er juillet 1992 interdit en effet expressément la photocopie à usage collectif sans autorisation des ayants droit. Or, cette pratique s’est généralisée notamment dans les établissements d’enseignement, provoquant une baisse brutale des achats de livres, au point que la possibilité même pour les auteurs de créer des œuvres nouvelles et de les faire éditer correctement est aujourd’hui menacée. En application de la loi du 11 mars 1957, il est interdit de reproduire intégralement ou partiellement le présent ouvrage, sur quelque support que ce soit, sans autorisation de l’éditeur ou du Centre Français d’Exploitation du Droit de Copie, 20, rue des Grands-Augustins, 75006 Paris. © Groupe Eyrolles, 2009, ISBN : 978-2-212-12458-3
=Labat FM.book Page V Vendredi, 19. juin 2009 4:01 16
Avant-propos Si vous lisez ce livre, c’est que votre objectif est sûrement de créer un jeu vidéo, c’est-àdire d’ordonner à l’ordinateur ou à la console d’effectuer un certains nombres de tâches.
La programmation de jeu vidéo Lors d’une utilisation quotidienne d’un ordinateur ou de votre console, vous n’avez nul besoin de programmer. Si vous devez faire une recherche sur l’Internet ou que vous voulez jouer à un jeu, vous vous contenterez d’utiliser un programme écrit par quelqu’un d’autre ; et ceci est tout à fait normal, nul besoin d’être plombier pour prendre un bain ! Définition Un programme informatique a pour but d’indiquer à un ordinateur la liste des étapes nécessaires à la réalisation 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
VI
Développement XNA pour la Xbox et le PC
Code intelligible, code machine Un ordinateur ne comprend que des instructions très simples : 1. Récupérer le contenu d’un emplacement mémoire. 2. Lui appliquer une opération mathématique basique. 3. Déplacer le résultat vers un autre emplacement mémoire. • En plus de diviser à l’extrême chaque tâche, pour être compris directement par l’ordinateur, vous devez lui parler en binaire, c’est-à-dire en une succession de 0 et 1. Imaginez donc la complexité du code machine qui se cache derrière le démineur de Microsoft… • Ce type de code n’étant pas du tout intelligible par un humain, il a donc fallu créer des langages possédant une syntaxe plus proche de notre langue ainsi que les outils nécessaires à la traduction du code écrit dans ces langages vers le code machine correspondant. Ces derniers sont généralement appelés compilateurs. • On distingue plusieurs types de langages : ceux dits de bas niveau et ceux de haut niveau. Plus un langage est de bas niveau, plus il se rapproche de la machine, c’est-à-dire que sa syntaxe est moins innée, que la gestion de la mémoire est plus difficile, etc. Prenons deux exemples. L’assembleur étant un langage de bas niveau, il faut traiter directement avec les registres du processeur, et il implique une bonne connaissance de l’architecture système. À l’inverse, le Visual Basic est un langage plus abordable qui n’est pas soumis aux mêmes contraintes que celles que nous venons de citer. • Il faut surtout garder en tête qu’un langage qui pourrait être classé de plus haut niveau n’est pas forcément plus facile à maîtriser qu’un autre. Tout dépend du programmeur, bien sûr, mais aussi du besoin : à cause de sa simplicité, le Visual Basic n’offre pas les mêmes possibilités d’optimisation que le C, par contre, il s’avère très pratique pour développer rapidement une application.
Les algorithmes Un algorithme est l’énoncé d’une suite d’opérations constituant une solution à un problème donné. On peut présenter toutes les actions de notre quotidien sous la forme algorithmique. Par exemple, pour la cuisson des pâtes : 1. Saler l’eau. 2. Porter à ébullition. 3. Plonger les pâtes. 4. Mélanger pour éviter qu’elles ne collent au fond. 5. Égoutter. 6. Rincer. Grâce à cet algorithme, vous pouvez aisément expliquer à quelqu’un la façon de cuire des pâtes, si besoin est.
=Labat FM.book Page VII Vendredi, 19. juin 2009 4:01 16
Avant-propos
Le langage algorithmique est un compromis entre notre langage courant et un langage de programmation. Ainsi, la compréhension d’une fonction d’un programme est plus aisée qu’en se plongeant directement dans le code.
XNA et son environnement Il existe une multitude de langage de programmation et de bibliothèques qui peuvent être utilisés pour programmer un jeu vidéo. Comment faire le bon choix ?
Pourquoi choisir XNA ? L’un des principaux critères qui peut motiver votre choix est la plate-forme cible. En effet, vous n’utiliserez pas forcément les mêmes outils pour créer un jeu pour Xbox 360 ou téléphone mobile. D’une manière générale, pour développer un jeu pour console, vous devrez utiliser un kit de développement adapté : la PSP possède son SDK utilisable en C++, celui de la Nintendo DS repose quant à lui sur le C. Du côté des PC, vous pouvez programmer un jeu vidéo dans un peu près n’importe quel langage. En ce qui concerne la partie graphique du jeu, deux solutions s’offrent à vous : la première consiste à utiliser des bibliothèques de très bas niveau telles que DirectX, OpenGL ou encore SDL. La seconde possibilité consiste à utiliser un moteur graphique comme OGRE ou Allegro. Elles est particulièrement intéressante car elle permet de gagner beaucoup de temps. XNA est une bibliothèque de bas niveau basée sur le framework Compact .Net dans son implémentation pour Xbox 360 (ou le lecteur multimédia Zune de Microsoft) et sur le framework .Net dans son implémentation pour PC.
Comprendre le framework .NET • Le framework .NET (prononcez « dotNet »), est un composant Windows apparu dans sa version 1.0 en 2002. Depuis, Microsoft a sorti régulièrement de nouvelles versions. Avec le système d’exploitation Windows XP, ce composant était facultatif. Cependant la version 3.0 du framework, .NET est directement intégré à Windows Vista. En détail Voici récapitulées les années de sortie des précédentes versions de notre framework : 1.1 en 2003 ; 2.0 en 2005 ; 3.0 en 2006 ; 3.5 en 2007.
• Il dispose de deux atouts majeurs pour simplifier le développement d’applications web ou Windows : le CLR (Common Language Runtime) et les bibliothèques de classes.
VII
=Labat FM.book Page VIII Vendredi, 19. juin 2009 4:01 16
VIII
Développement XNA pour la Xbox et le PC
• Le CLR est une machine virtuelle (bien que Microsoft préfère utiliser le terme runtime) utilisée pour exécuter une application .NET. Il possède, entre autres, un composant appelé JIT (Just In Time, c’est-à-dire juste à temps), qui compile du code MSIL (Microsoft Intermediate Language) vers du code compréhensible par la machine. Ainsi, tout langage disposant d’un compilateur qui produit du code MSIL (les spécifications techniques sont disponibles à cette adresse : http://www.ecma-international.org/publications/ standards/Ecma-335.htm/) est exécutable par le CLR et bénéficie des possibilités offertes par la plate-forme. Il est donc possible de choisir un langage parmi un grand nombre (C#, C++, VB.NET, J#, etc.), le choix ne dépendant plus forcément des performances mais plutôt d’une affaire de goût. Le CLR comporte également une multitude d’autres technologies dont vous ne saisiriez peut-être pas l’intérêt pour le moment, mais que nous aborderons plus tard dans cet ouvrage. MSIL Langage ressemblant à de l’assembleur, MSIL ne comporte aucune instruction propre à un système d’exploitation ou à du matériel.
Le framework .NET met également à la disposition du programmeur plus de 2 000 classes utilitaires, qui lui permettent de gagner un temps précieux lors du développement. Ainsi, manipulation de chaînes de caractères, communication réseau, accès aux données sont choses faciles à réaliser. À chaque nouvelle version du framework, la bibliothèque de classes s’étoffe davantage et les fonctionnalités disponibles sont de plus en plus performantes.
XNA : faciliter le développement de jeu vidéo Le framework XNA (XNA’s Not Acronymed) est constitué de plusieurs bibliothèques .NET et permet un développement multi-plate-forme : les classes fournies par XNA permettent au programmeur de développer un jeu pour Windows puis de le porter très facilement pour qu’il soit utilisable sur Xbox 360 ou sur le lecteur multimédia Zune. L’un des buts de XNA est de simplifier au maximum le développement de jeu vidéo. Par exemple, si vous avez déjà eu une expérience dans le développement avec l’api DirectX ou OpenGL, vous savez certainement qu’écrire l’initialisation de votre programme vous prendrait un certain temps alors qu’avec XNA tout est automatique. C’est précisément là que réside tout l’intérêt du framework : avec XNA, il vous suffit seulement d’écrire quelques lignes de code très facilement compréhensibles pour créer un jeu complet. Bon à savoir Soulignons également que le framework XNA est livré avec ce que l’on appelle des Starter Kit. Ces petits projets de jeu vidéo montrent les possibilités offertes ainsi que le niveau d’accessibilité du développement.
=Labat FM.book Page IX Vendredi, 19. juin 2009 4:01 16
Avant-propos
Officiellement, XNA ne peut être utilisé qu’avec le langage de programmation C#. En pratique, vous pouvez également réaliser un jeu avec XNA en VB.NET, mais vous ne pourrez pas utiliser tous les composants offerts par le framework. Version XNA 3.0 est disponible depuis le 30 octobre 2008, c’est sur cette version que ce livre se focalise.
C#, langage de programmation de XNA Langage de programmation orienté objet à typage fort, C# (prononcez « C-Sharp ») a fait son apparition avec la plate-forme .NET. Il est très proche à la fois du Java et du C++. Ses détracteurs le qualifient souvent de copie propriétaire de Java. Java Très répandu dans le monde du logiciel libre, ce langage s’exécute lui aussi sur une machine virtuelle. À l’heure actuelle et selon des sondages qui paraissent régulièrement sur l’Internet, il s’agit du langage le plus populaire parmi les développeurs.
Tout comme le framework .NET dont il est indissociable, le langage C# est régulièrement mis à jour et se voit ajouter des améliorations syntaxiques ou de conception.
Choisir son environnement de développement intégré Pour utiliser XNA ou, d’une manière plus générale, programmer dans un langage compatible .Net, vous aurez besoin d’un EDI (Environnement de Développement Intégré). Microsoft en propose toute une gamme comprenant : • Visual Studio Express. • Visual Studio Standard. • Visual Studio Professional. • Visual Studio Team System. Chaque version vise un public différent, les versions Express (il en existe une pour le langage C#, une pour le C++, une pour le VB et une pour le développement web) sont gratuites et s’adressent au développeur amateur tandis que la version Team System est orientée pour le développement professionnel en équipe. XNA 3.0 est compatible avec les versions de Visual Studio 2008. Dans ce livre, nous utiliserons la version Microsoft Visual C# Express 2008. Vous connaissez maintenant tous les outils nécessaires pour commencer, alors bonne lecture et bienvenue dans le monde du C# et de XNA !
IX
=Labat FM.book Page X Vendredi, 19. juin 2009 4:01 16
X
Développement XNA pour la Xbox et le PC
À qui s’adresse le livre ? Ce livre s’adresse à tous ceux qui désirent créer des jeux pour PC, pour Xbox 360 ou pour le Zune sans avoir d’expérience préalable dans ce domaine ou même dans celui plus vaste de la programmation. En effet, nous y présentons les notions de bases du C# nécessaires à la compréhension de XNA. Ainsi, ce livre vous sera utile si, étudiant en programmation, vous souhaitez découvrir l’univers du développement de jeux vidéo ; si vous travaillez au sein d’un studio indépendant ou en tant que freelance et que vous souhaitez vous former aux spécificités de développement pour Xbox ; ou si, tout simplement, vous êtes curieux de vous initier au développement de jeu et que vous avez choisi XNA. Cependant, nous vous conseillons tout de même de vous munir d’un ouvrage sur le langage de programmation C# : ce livre ne constitue pas un document de référence sur ce langage, nous ne verrons ici que ce qui sera utile à la compréhension du framework XNA, et certaines facettes du langage seront mieux détaillées dans un ouvrage spécialisé.
Structure de l’ouvrage Le chapitre 1 présente les notions de base du langage de programmation C#, qui vous seront utiles dès le chapitre 2 à la création d’une première application avec XNA. Nous attaquerons les choses sérieuses dans le chapitre 3 en apprenant à afficher de premières images à l’écran puis, dans le chapitre 4, nous apprendrons à récupérer les entrées utilisateur sur le clavier, la souris ou la manette de la Xbox 360. Ces notions seront mises en pratique avec la création d’un clone de Pong dans le chapitre 5. Le chapitre 6 poussera plus loin les fonctions d’affichage d’images dans XNA. Dans le chapitre 7, vous étofferez votre jeu en lui ajoutant un environnement sonore qu’il s’agisse des sons ou de morceaux de musique. Puis, dans le chapitre 8, vous découvrirez les techniques de lecture ou d’écriture de fichiers qui entrent en jeu dans les fonctionnalités de sauvegarde. Dans le chapitre 9, vous vous écarterez un peu du monde de XNA pour rejoindre celui des sciences cognitives et plus particulièrement l’implémentation d’un algorithme de recherche de chemin. Le chapitre 10 abordera également un domaine qui n’est pas propre à XNA : la gestion de la physique. Nous verrons donc comment implémenter un moteur physique. Dans le chapitre 11, le dernier à utiliser des exemples en deux dimensions, vous découvrirez 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
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
[email protected]
XI
=Labat FM.book Page XII Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page XIII Vendredi, 19. juin 2009 4:01 16
Table des matières
Avant-propos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
V
La programmation de jeu vidéo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Code intelligible, code machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
V VI VI
XNA et son environnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pourquoi choisir XNA ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comprendre le framework .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XNA : faciliter le développement de jeu vidéo . . . . . . . . . . . . . . . . . . . . . . . . C#, langage de programmation de XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Choisir son environnement de développement intégré . . . . . . . . . . . . . . . . . .
VII VII VIII IX IX
À qui s’adresse le livre ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
X
Structure de l’ouvrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
X
Remerciements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XI
VII
CHAPITRE 1
Débuter en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
Créez votre premier programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
Les types de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Organisation de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Opérations de base sur les variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
Les instructions de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Commenter son code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les conditions : diversifier le cycle de vie des jeux . . . . . . . . . . . . . . . . . . . .
3 4 8 11 11 11
=Labat FM.book Page XIV Vendredi, 19. juin 2009 4:01 16
XIV
Développement XNA pour la XBox et le PC
Les fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Différencier fonction et procédure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Écrire une première procédure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Écrire une première fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14 15 15 17
Les classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comprendre les classes et les objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utiliser un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Qu’est ce qu’un espace de noms ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Créer une classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18 18 18 19 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Structure du framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Structure du code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29 29 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Qu’est-ce qu’un sprite ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Afficher un sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Afficher plusieurs sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un sprite en mouvement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35 36 37 40 41
Une classe pour gérer vos sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Créer une classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utiliser la classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Classe héritée de Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45 45 47 50
=Labat FM.book Page XV Vendredi, 19. juin 2009 4:01 16
Table des matières
XV
Un gestionnaire d’images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les boucles en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Écriture du gestionnaire d’images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mesure des performances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55 57 58 60 63
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
65
55
CHAPITRE 4
Interactions avec le joueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
67
Utiliser les périphériques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Le clavier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La souris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La manette de la Xbox 360 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilisation de périphériques spécialisés . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
67 67 70 71 73
Les services avec XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les interfaces en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comment utiliser les services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les méthodes génériques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74 75 77
Toujours plus d’interactions grâce à la GUI . . . . . . . . . . . . . . . . . . . . . . . .
80
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
81
73
CHAPITRE 5
Cas pratique : programmer un Pong . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
Avant de se lancer dans l’écriture du code . . . . . . . . . . . . . . . . . . . . . . . . . . Définir le principe du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formaliser en pseudo-code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
Développement du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Création du projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . L’arrière-plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les raquettes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La balle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Améliorer l’intérêt du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85 85 86 88 91 95
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
83 84
=Labat FM.book Page XVI Vendredi, 19. juin 2009 4:01 16
XVI
Développement XNA pour la XBox et le PC
CHAPITRE 6
Enrichir les sprites : textures, défilement, transformation, animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
Préparation de votre environnement de travail . . . . . . . . . . . . . . . . . . . . . .
97
Texturer un objet Rectangle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modifier la classe Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Faire défiler le décor : le scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Créer des animations avec les sprites sheets . . . . . . . . . . . . . . . . . . . . . . . . . . Varier la teinte des textures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Opérer des transformations sur un sprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99 100 105 108 112 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Créer un projet sonore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lire les fichiers créés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lire les fichiers en streaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ajouter un effet de réverbération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
131 132 135 138 139 140
Le son avec la nouvelle API SoundEffect . . . . . . . . . . . . . . . . . . . . . . . . . . . Lire un son . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lire un morceau de musique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
141 141 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les espaces de stockage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sérialisation et désérialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
145 145 148
Les exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
148
=Labat FM.book Page XVII Vendredi, 19. juin 2009 4:01 16
Table des matières
Les Gamer Services : interagir avec l’environnement . . . . . . . . . . . . . . . . Dossier de l’utilisateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les méthodes asynchrones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La GamerCard : la carte d’identité du joueur . . . . . . . . . . . . . . . . . . . . . . . . . Version démo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La sauvegarde en pratique : réalisation d’un éditeur de cartes . . . . . . . . Identifier les besoins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Chemin du dossier de jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gérer les dossiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Manipuler les fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Écrire dans un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lire un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sérialiser des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Désérialiser des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les Content Importers, une solution compatible avec la Xbox 360 . . . . En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XVII 152 152 154 158 160 162 162 168 169 172 174 176 178 180 181 184
CHAPITRE 9
Pathfinding : programmer les déplacements des personnages
185
Les enjeux de l’intelligence artificielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
185
Comprendre le pathfinding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
186
L’algorithme A* : compromis entre performance et pertinence . . . . . . . Principe de l’algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implanter l’algorithme dans un jeu de type STR . . . . . . . . . . . . . . . . . . . . . . Cas pratique : implémenter le déplacement d’un personnage sur une carte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Préparation : identifier et traduire les actions du joueur . . . . . . . . . . . . . . . . . Créer le personnage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implémenter l’algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
187 187 190 200 200 201 204 206
CHAPITRE 10
Collisions et physique : créer un simulateur de vaisseau spatial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
207
Comment détecter les collisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Créer les bases du jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Établir une zone de collision autour des astéroïdes . . . . . . . . . . . . . . . . . . . . .
207 208 213
=Labat FM.book Page XVIII Vendredi, 19. juin 2009 4:01 16
XVIII
Développement XNA pour la XBox et le PC
Simuler un environnement spatial : la gestion de la physique . . . . . . . . . Choisir un moteur physique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Télécharger et installer FarseerPhysics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prise en main du moteur physique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les collisions avec FarseerPhysics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
217
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
230
217 219 222 227
CHAPITRE 11
Le mode multijoueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231
Jouer à plusieurs sur le même écran . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231
Du mode solo au multijoueur : la gestion des caméras . . . . . . . . . . . . . . . Créer un jeu solo avec effet de scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adapter les caméras au multijoueur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Personnaliser les différentes vues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
232
Le multijoueur en réseau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . S’appuyer sur la plate-forme Live . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implémenter les fonctionnalités de jeu en réseau . . . . . . . . . . . . . . . . . . . . . .
248
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
257
232 237 244 248 249
CHAPITRE 12
Les bases de la programmation 3D . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
259
L’indispensable théorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Le système de coordonnées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Construire des primitives à partir de vertices . . . . . . . . . . . . . . . . . . . . . . . . . Les vecteurs dans XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les matrices et les transformations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gérer les effets sous XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comprendre la projection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
259 259 260 262 263 263 264
Dessiner des formes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La caméra et la matrice de projection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La matrice de vue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Des vertices à la forme à dessiner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
265 266 267
Déplacer la caméra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
277
Appliquer une couleur à un vertex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
279
265
=Labat FM.book Page XIX Vendredi, 19. juin 2009 4:01 16
Table des matières
XIX
Plaquer une texture sur un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Texturer une face d’un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Texturer un objet entier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
281 281 285
Déplacer un objet avec les transformations . . . . . . . . . . . . . . . . . . . . . . . . .
291
Jouer avec les lumières . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les différents types de lumière . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Éclairer une scène pas à pas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
294 294 295
Charger un modèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
299
En résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
301
CHAPITRE 13
Améliorer le rendu avec le High Level Shader Language . . . . . .
303
Les shaders et XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vertex shaders et pixel shaders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ajouter un fichier d’effet dans XNA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
303 304 305
Syntaxe du langage HLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les variables HLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les structures de contrôle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les fonctions fournies pas le langage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sémantiques et structures pour formats d’entrée et de sortie . . . . . . . . . . . . . Écrire un vertex shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Écrire un pixel shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Finaliser un effet : les techniques et les passes . . . . . . . . . . . . . . . . . . . . . . . .
306 306 308 308 308 309 310 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
XX
Développement XNA pour la XBox et le PC
L’éditeur de texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les extraits de code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Refactoriser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Déboguer une application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Raccourcis clavier utiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
324 326 327 328 330
CHAPITRE B
Les bienfaits de la documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
331
L’incontournable MSDN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ressources sur le Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Générer de la documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
331
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
337
333 334
=Labat FM.book Page 1 Vendredi, 19. juin 2009 4:01 16
1 Débuter en C#
Ce premier chapitre a pour but de vous guider dans vos premiers pas de programmeurs et notamment avec le langage C#. Commençant par la découverte des types de données et allant jusqu’à la création de vos propres classes, ce chapitre constitue le minimum vital à connaître avant de s’attaquer à la création d’un jeu. Ne vous inquiétez pas si nous n’avons pas tout de suite recours à XNA, mais commençons par le mode console. En effet, ce dernier est particulièrement adapté pour l’apprentissage de C#.
Créez votre premier programme Avant de nous lancer dans l’apprentissage du C#, découvrons ensemble l’environnement dans lequel nous allons travailler. Tout d’abord, démarrez Visual C# Express (figure 1-1). Ensuite, créez un nouveau projet console en cliquant sur Fichier puis Nouveau Projet (figure 1-2).
=Labat FM.book Page 2 Vendredi, 19. juin 2009 4:01 16
2
Développement XNA pour la XBox et le PC
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
Le logiciel a automatiquement généré le code suivant dans un fichier Program.cs : using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace PremierProgramme { class Program { static void Main(string[] args) { } } }
Pour compiler l’application et l’exécuter (lorsque c’est possible), cliquez sur Générer puis Générer la solution ou utilisez le raccourci clavier F5. Tout au long de ce chapitre, nous allons analyser les possibilités qu’offre cette portion de code. Ne fermez surtout pas Visual Studio, vous serez amené à l’utiliser parallèlement à la lecture de ce chapitre.
Les types de données Dans cette partie, nous allons dans un premier temps nous intéresser à la manière dont s’organise globalement le stockage des données en informatique, puis nous passerons en revue les différents types de données qui nous seront utiles pour la programmation de jeux.
Organisation de la mémoire Avant de se jeter tête la première dans le code, il est nécessaire de voir (ou de revoir) quelques notions de base du fonctionnement de l’ordinateur et tout particulièrement celui de la mémoire. Ainsi, vous serez amené à travailler avec les deux grands types de mémoires : • La mémoire vive – Généralement appelée RAM (Random Access Memory), elle est volatile. Ce terme signifie qu’elle ne permet de stocker des données que lorsqu’elle est alimentée électriquement. Ainsi, dès que vous redémarrez votre ordinateur, sa RAM perd tout son contenu. Lire des données présentes sur ce type de mémoire se fait plus rapidement que lire des données présentes sur de la mémoire physique. • La mémoire physique – Cette mémoire correspond à votre disque dur ou à tous les périphériques physiques de stockage de données (DVD-ROM, carte mémoire, etc.). Elle n’est pas volatile, son contenu est conservé même lorsqu’elle n’est plus alimentée électriquement.
3
=Labat FM.book Page 4 Vendredi, 19. juin 2009 4:01 16
4
Développement XNA pour la XBox et le PC
Sa capacité de stockage est souvent plus élevée que celle de la mémoire vive. Les informations que l’on stocke dans la RAM s’appellent variables, tout simplement parce que leur valeur peut changer au cours du temps.
Les variables Le C# est un langage à typage fort : il existe plusieurs types de variables, chaque type ayant des caractéristiques bien précises (utilisation mémoire, possibilité, précision, etc.). En C#, comme dans la vie, on ne mélange pas les torchons et les serviettes. Stocker des nombres entiers
Les premiers types de variables que nous allons découvrir servent à stocker les nombres entiers. Leur particularité se situe au niveau de leur capacité de stockage, et donc de leur occupation en mémoire. Tableau 1-1 Les types entiers Type
Stockage
Valeur minimale
Valeur maximale
Byte
1 octet
0
255
Short
2 octets
– 32768
32767
Int
4 octets
–
231
231-1
Long
8 octets
– 9.2 ¥ 1018
9.2 ¥ 1018
Une variable se déclare de la façon suivante : type identificateur;
Par exemple, pour déclarer une variable entière correspondant au nombre de vies restantes de joueur, il faut procéder de la manière suivante : short nombreDeVies;
Il faut respecter certaines règles dans le nommage des identificateurs : • Vous devez faire attention à ce que le premier caractère soit une lettre majuscule ou minuscule ou un underscore (_). • Pour tous les autres caractères, vous pouvez utiliser soit une lettre majuscule ou minuscule, soit un underscore ou alors un chiffre. À ce stade, la variable étant uniquement déclarée, vous ne pouvez pas l’utiliser. Faites le test en essayant de compiler le code suivant : namespace PremierProgramme { class Program {
=Labat FM.book Page 5 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
static void Main(string[] args) { short nombreDeVies; Console.WriteLine(nombreDeVies); } } }
Dans ce programme, une variable de type short est déclarée et son contenu s’affiche dans la console. Cependant, la compilation a échoué (figure 1-3) et ceci est tout à fait normal. En effet, la variable a uniquement été déclarée, elle n’a pas été initialisée, c'est-à-dire qu’elle n’a pas encore reçu de valeur.
Figure 1-3
La compilation du programme a échoué
L’initialisation d’une variable est très facile à réaliser : identificateur = valeur;
À présent, remplacez le code précédent par celui ci-dessous et compilez-le : namespace PremierProgramme { class Program { static void Main(string[] args) { short nombreDeVies; nombreDeVies = 7; Console.WriteLine(nombreDeVies); Console.ReadLine(); } } }
Cette fois-ci, vous constatez que le compilateur ne signale aucune erreur et que la valeur qui a été affectée à la variable s’affiche correctement dans la console. Notez que la ligne Console.Read(); qui n’était pas présente dans l’exemple précédent, permet de figer la console tant que l’utilisateur n’appuie sur aucune touche du clavier. Sans elle, la fenêtre s’ouvrirait et se fermerait toute seule en un éclair.
5
=Labat FM.book Page 6 Vendredi, 19. juin 2009 4:01 16
6
Développement XNA pour la XBox et le PC
Déclarer et initialiser une variable peut se faire sur une seule et même ligne : short nombreDeVies = 7;
Enfin, il est également possible de déclarer et d’initialiser plusieurs variables sur la même ligne : short nombreDeVies = 7, score = 0;
Les booléens : vrai ou faux
Une variable de type booléen peut avoir deux états : vrai ou faux, soit respectivement true ou false. Elle s’utilise de la manière suivante : bool test = true;
Les booléens sont issus de l’algèbre de Boole. Les conditions et tests logiques sont basés sur eux. Découverte des nombres à virgule : les nombres réels
Comme pour les entiers, il existe plusieurs types de variables pour les nombres réels. Tableau 1-2 Les types réels Type
Stockage
Valeur minimale 10–45
Valeur maximale 3.4 ¥ 1038
float
4 octets
1.4 ¥
double
8 octets
4.9 ¥ 10–324
1.8 ¥ 10308
decimal
16 octets
8 ¥ 10–28
8 ¥ 1028
Il faut utiliser le point (.) comme séparateur entre la partie réelle et la partie décimale de votre nombre. Par exemple : double nombreReel = 4.56;
Notez qu’en utilisant les types float et double, vous devrez faire face à un problème de précision. Testez par exemple le programme suivant : namespace PremierProgramme { class Program { static void Main(string[] args) { double total = 0; while (total < 1) total += 0.0001; Console.WriteLine(total); Console.ReadLine(); } } }
=Labat FM.book Page 7 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
Le mot-clé while correspond à une structure algorithmique que nous étudierons plus tard. Ce programme utilise une variable total de type double et l’initialise à 0. Tant que la valeur totale est inférieure à 1, il faut lui ajouter 0,0001. À l’exécution, voici ce qui s’affiche dans la console : 1,00009999999991
À présent, changez le type de total et déclarez plutôt la variable en tant que decimal. namespace PremierProgramme { class Program { static void Main(string[] args) { decimal total = 0; while (total < 1) total += (decimal)0.0001; Console.WriteLine(total); Console.ReadLine(); } } }
Cette fois-ci, voici le resultat qui s’affiche à l’écran : 1
Le type decimal prend plus de place en mémoire que les types float et double, il nécessite également un temps de traitement plus long. Dans vos jeux, vous serez souvent amené à manipuler des nombres réels. Lorsque vous effectuerez des tests sur ces variables, n’oubliez jamais que cette erreur de précision peut entraîner des erreurs de logique que vous n’auriez pas prévues. Lorsque vous choisissez le type d’une variable, analysez toujours au préalable vos besoins et soupesez bien les avantages et inconvénients de chaque possibilité ! Stocker une lettre ou un signe avec char
Pour stocker un caractère, il existe le type char. Celui-ci est codé sur deux octets en mémoire. Il s’utilise de la manière suivante : namespace PremierProgramme { class Program { static void Main(string[] args) {
7
=Labat FM.book Page 8 Vendredi, 19. juin 2009 4:01 16
8
Développement XNA pour la XBox et le PC
char lettre = 'a'; Console.WriteLine(lettre); Console.ReadLine(); } } }
Attention à bien utiliser des guillemets simples (ou apostrophes) « ' » et pas des guillemets doubles « " ». Les chaînes de caractères
Une variable de type char ne correspond qu’à un seul caractère ; à l’inverse, une chaine de caractère en contiendra un ou plusieurs. Pour en déclarer une, il faut utiliser le mot-clé string. Voici comment l’utiliser : namespace PremierProgramme { class Program { static void Main(string[] args) { string chaine = "Test"; Console.WriteLine(chaine); Console.ReadLine(); } } }
Cette fois-ci, il faudra bien utiliser les guillemets doubles. Les constantes
Tous les types que nous avons vus jusqu’à maintenant peuvent être déclarés en tant que constante grâce au mot-clé const. Bien évidemment, et comme son nom l’indique, la valeur d’une constante ne peut pas être modifiée durant le cycle de vie du programme. const int N = 7;
Opérations de base sur les variables Le tableau 1-3 répertorie les opérations arithmétiques de base qui peuvent être utilisées sur les nombres en C# :
=Labat FM.book Page 9 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1 Tableau 1-3 Opérateurs de base du langage Opération
Description
A + B
Addition de A et de B
A – B
Soustraction de B à A
A * B
Multiplication de A par B
A / B
Division de A par B
A % B
Reste de la division de A par B
Le programme suivant met en application ces opérations (le résultat est présenté sur la figure 1-4) : namespace PremierProgramme { class Program { static void Main(string[] { int A = 6; int B = 3; Console.WriteLine(A + Console.WriteLine(A Console.WriteLine(A * Console.WriteLine(A / Console.WriteLine(A % Console.ReadLine(); } }
args)
B); B); B); B); B);
Figure 1-4
Test des opérations arithmétiques
Vous pouvez stocker le résultat de chaque calcul dans une variable. A = A + B;
Ce type d’opération peut également se factoriser de la manière suivante : A += B;
9
=Labat FM.book Page 10 Vendredi, 19. juin 2009 4:01 16
10
Développement XNA pour la XBox et le PC
Vous pouvez utiliser ce genre de raccourci avec tous les opérateurs arithmétiques. Les opérations de pré et post-incrémentations ou décrémentations sont également une bonne manière de gagner du temps. Leur but est de raccourcir l’écriture de lignes telles que : A = A + 1;
En utilisant la post-incrémentation, la ligne précédente devient : A++; Tableau 1-4 Opérateurs d’incrémentation et de décrémentation Opération
Description
A++
Post-incrémentation de A
++A
Pré-incrémentation de A
A--
Post-décrémentation de A
--A
Pré-décrémentation de A
La post-incrémentation se fait après l’exécution d’une ligne d’instruction, alors que la pré-incrémentation aura lieu avant. Un exemple valant mieux qu’un long discours, compilez le programme suivant et observez les effets de chacune des opérations (figure 1-5). namespace PremierProgramme { class Program { static void Main(string[] args) { int A = 6; Console.WriteLine(A++); Console.WriteLine(++A); Console.WriteLine(A--); Console.WriteLine(--A); Console.ReadLine(); } } }
Figure 1-5
Incrémentation et décrémentation
=Labat FM.book Page 11 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
Les instructions de base Vous ne pouvez pas créer un programme, et a fortiori un jeu, juste en déclarant des variables, ou si vous y arrivez, le résultat ne serait pas réellement intéressant. Dans cette partie, nous allons nous intéresser aux instructions de base du langage grâce auxquelles vos programmes vont se complexifier.
Commenter son code Que vous soyez amené à partager du code avec d’autres personnes ou non, il est toujours très important d’être clair et facilement compréhensible lorsque vous programmez. Une bonne pratique à adopter est donc de commenter votre code. En C#, il existe trois types de commentaires : // Commentaire sur une ligne /* * Commentaire sur plusieurs lignes */ /// Commentaire pour la documentation automatique (voir Annexe A)
S’il est important de commenter votre code, attention cependant à ne pas tomber pas dans l’excès : ne commentez que ce qui est réellement utile. Essayez d’avoir une vision critique vis-à-vis de votre code, celle de quelqu’un qui n’a pas mené la réflexion qui vous a fait aboutir à tel ou tel choix. Ajouter trop de commentaires inutiles risque de rendre vos fichiers sources illisibles, et de vous faire perdre du temps.
Les conditions : diversifier le cycle de vie des jeux L’écriture de tests logiques et de conditions est la base de la programmation. Voici la structure algorithme d’un test simple : SI CONDITION EST VRAIE ALORS FAIRE… FIN SI
En C#, le mot-clé utilisé pour faire un test est le mot-clé if. Voici un exemple d’utilisation simple : if (true) { Console.WriteLine("Bien!"); }
Si le code à exécuter dans le cas où la condition est vraie et ne tient que sur une ligne, il est également possible d’écrire : if (true) Console.WriteLine("Bien!");
11
=Labat FM.book Page 12 Vendredi, 19. juin 2009 4:01 16
12
Développement XNA pour la XBox et le PC
Mais ce test n’a pas réellement d’intérêt. Dans l’exemple suivant, le test porte sur le nombre de vies restantes à un joueur. S’il n’en a plus, le programme lui signale qu’il est mort. namespace PremierProgramme { class Program { static void Main(string[] args) { short nombreDeVies = 1; if (nombreDeVies == 0) Console.WriteLine("Vous êtes mort"); nombreDeVies--; if (nombreDeVies == 0) Console.WriteLine("Vous êtes mort"); Console.ReadLine(); } } }
Voici la liste des opérateurs conditionnels : Tableau 1-5 Opérateurs conditionnels Opérateur
Description
==
Test d’égalité
!=
Test de différence
>
Strictement supérieur
>=
Supérieur ou égal
<
Strictement inférieur
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
Différencier fonction et procédure Vous ne pouvez pas écrire tout un programme ou tout un jeu, même s’il contient peu de lignes de codes, dans le même fichier : cela serait illisible et impossible à maintenir. Vous feriez mieux d’utiliser les fonctions et les procédures. Une fonction exécute certaines actions (des calculs, de l’extraction de données, etc.), puis renvoie un résultat. Par exemple, la fonction Carre() attend un nombre en argument, et renvoie ce même nombre élevé au carré. Dans un jeu, vous pourriez avoir une fonction GetFriends() qui retournerait la liste des amis de votre personnage. Contrairement à une fonction, une procédure ne retournera pas de résultat. Ainsi, la procédure AfficherMenu() affichera un menu dans la console, mais ne retournera pas de valeur. Dans votre jeu, la procédure Draw() contient les mécanismes de dessin de votre jeu, mais ne retourne pas de valeur. Utiliser une fonction ou une procédure vous permet de gagner du temps en factorisant le nombre de lignes de code que vous écrivez. De plus, créer des fonctions ou des procédures offre l’avantage non négligeable d’écrire du code réutilisable. Ainsi, au fur et à mesure de vos projets, vous vous construirez une véritable bibliothèque de « briques » réutilisables.
Écrire une première procédure Tout d’abord, sachez que jusqu’ici vous avez déjà utilisé plusieurs procédures, peut-être à votre insu si ce livre constitue votre première expérience de programmation. L’extrait de code ci-dessous comporte la fonction principale d’un programme. Si vous l’exécutez, le message « Bonjour »s’affiche dans la console, puis dès que vous appuyez sur une touche, la console se ferme. namespace PremierProgramme { class Program { static void Main(string[] args) { Console.WriteLine("Bonjour"); Console.ReadLine(); } } }
Dans cette petite portion de code se cachent deux procédures. La procédure Main, que vous définissez et la procédure WriteLine, que vous appelez. En pratique Main est le point d’entrée de l’application : c’est ici que tout commence.
15
=Labat FM.book Page 16 Vendredi, 19. juin 2009 4:01 16
16
Développement XNA pour la XBox et le PC
Appeler une procédure se fait donc très simplement, il suffit d’écrire son nom, et si besoin, de lui passer des arguments. Dans l’exemple précédent, vous appelez WriteLine en lui passant une variable de type string, qui correspond au texte à afficher dans la console. Il est temps d’écrire votre première procédure. Celle-ci servira à afficher quelques lignes de Lorem Ipsum (faux texte bien connu des développeurs web). La déclaration d’une procédure se fait de la manière suivante : Void NomDeLaProcedure(typeA argumentA, ...) { }
Le mot-clé void signifie qu’il n’y a pas de valeur en retour, ce qui correspond bien à la définition d’une procédure. Le nom d’une procédure est régi par les mêmes règles qui s’appliquent aux noms de variables. Le nombre d’arguments à passer à la fonction dépend bien évidemment de vos besoins. Sachez qu’il est également possible de passer les arguments par référence plutôt que par valeur. Pour l’instant tous les passages que nous allons voir se font par valeur (nous aborderons les autres plus tard). Dernière règle à respecter lors de la déclaration d’une procédure : son corps doit être entouré de deux accolades ouvrante « { » et fermante « } ». Voici donc la définition et, bien sûr, l’appel de cette première procédure : namespace PremierProgramme { class Program { static void Main(string[] args) { AfficherLoremIpsum(); Console.ReadLine(); } static void AfficherLoremIpsum() { Console.WriteLine("Lorem ipsum dolor sit amet, consectetuer adipiscing ➥ elit."); Console.WriteLine("Aliquam pretium, leo non scelerisque porttitor, ➥ tellus turpis feugiat lacus, sed ullamcorper nisl felis non nibh."); Console.WriteLine("Fusce posuere mollis justo."); } } }
=Labat FM.book Page 17 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
Renvoi Remarquez la présence du mot-clé static devant void. Nous reviendrons sur la signification de ce motclé dans la section « Créer une classe » de ce chapitre.
Écrire une première fonction Depuis le début de la lecture de ce livre, vous utilisez la fonction ReadLine. Celle-ci lit l’entrée clavier et renvoie une chaîne de caractères. Il est donc possible d’imaginer sa définition : string ReadLine() { // ... return valeur; }
Les règles pour la définition d’une fonction sont les mêmes que celles qui s’appliquent aux procédures, sauf pour le type. En effet, vous n’êtes pas restreint au type void, mais vous pouvez utiliser celui que vous voulez. Le code suivant contient la définition d’une fonction qui renvoie la valeur absolue d’un nombre passé en argument, ainsi que l’utilisation de cette fonction : namespace PremierProgramme { class Program { static int ValeurAbsolue(int number) { if (number < 0) return -number; else return number; } static void Main(string[] args) { int a = ValeurAbsolue(-45); Console.WriteLine(a); Console.WriteLine(ValeurAbsolue(5)); Console.ReadLine(); } } }
Cet exemple n’a de but autre que pédagogique. Sachez que plusieurs milliers de fonctions sont fournies par le framework .NET, répondant à des besoins extrêmement variés. Vous
17
=Labat FM.book Page 18 Vendredi, 19. juin 2009 4:01 16
18
Développement XNA pour la XBox et le PC
avez également la possibilité de vous procurer des fonctions spécifiques sur l’Internet ou bien de créer les vôtres et les communiquer à d’autres développeurs. Lorsque vous utilisez une procédure ou bien une fonction, Visual Studio vous fournit des informations sur le type de la fonction ou les paramètres attendus (figure 1-7).
Figure 1-7
Visual Studio sait se rendre très utile
Les classes Piliers du C# et donc de XNA, les classes et les objets sont indispensables dans la création de n’importe quel programme : chaque programme écrit en C# (qu’il s’agisse d’un jeu, d’une application Windows, d’une application console, etc.) en possède au moins une. Dans cette section, nous nous intéresserons tout d’abord à la différence entre une classe et un objet. Nous pourrons alors voir comment utiliser un objet, et enfin, écrire votre première classe.
Comprendre les classes et les objets Avant de nous lancer dans les aspects techniques, il est nécessaire de bien cerner les notions de classes et d’objets. Pour prendre un exemple concret, une classe est comparable à une recette de gâteau et un objet à un gâteau. En somme, en réalisant un gâteau, vous avez donné vie à votre recette. En programmation orientée objet (POO), l’objet gâteau est alors qualifié d’instance de la classe recette de gâteau. Un objet possède des propriétés et des méthodes (il peut s’agir de fonctions ou de procédures). Le tableau ci-dessous liste celles d’un objet instancié à partir de la classe Homme. Tableau 1-7 Différence entre propriétés et méthodes Propriétés
Méthodes
Taille, force, agilité, etc.
Marcher, boire, se défendre, etc.
Utiliser un objet Vous avez déjà utilisé des objets sans le savoir. Derrière les types de données que nous avons vus précédemment se cachent des classes. Ainsi, pour déclarer une chaîne de caractères, il est également possible d’écrire : String chaine = "Test";
=Labat FM.book Page 19 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
String (avec un S majuscule) correspond donc à la classe.
Les propriétés et méthodes d’un objet String sont visibles en inscrivant un point (.), puis en choisissant ce à quoi vous voulez accéder. Console.WriteLine(chaine.Length); // Longueur de la chaîne Console.WriteLine(chaine.Substring(1, 2)); // Sous-chaîne commençant à la position 1 ➥ et d'une longueur de 2 caractères
Dans le framework .Net, un grand nombre de classes est disponible et répondront à bon nombre de vos besoins : File pour la gestion de fichiers, Directory pour la gestion de répertoire, Socket pour les communications réseaux, etc. Prenons, par exemple, la classe TimeSpan, qui représente un intervalle de temps : TimeSpan time = new TimeSpan(26, 14, 30, 10); Console.WriteLine(time.ToString());
L’initialisation d’un objet se fait via le mot-clé new, qui appelle le constructeur de la classe TimeSpan. Un constructeur est une fonction un peu spéciale, nous y reviendrons lors de la création de votre première classe.
Cas particulier Si vous parcourez un peu les classes fournies par le framework (en utilisant IntelliSense, reportez-vous l’annexe A pour plus de détails), vous découvrirez sûrement la classe Math, qui est un peu spéciale. En effet, la création d’un objet de type Math est impossible. En fait, cette classe est statique, c'est-à-dire qu’elle n’est pas instanciable. Cependant, elle possède tout de même des propriétés et des méthodes, elles aussi statiques, qui sont accessibles de la manière suivante :
Console.WriteLine(Math.PI); Console.WriteLine(Math.Abs(-15));
Qu’est ce qu’un espace de noms ? Un espace de noms (namespace) organise les classes de manière logique : tout comme les répertoires permettent de classer les fichiers, les espaces de noms servent à organiser les classes. Ainsi, il ne peut y avoir deux classes du même nom dans le même espace de noms. Cependant, deux classes peuvent tout à fait porter le même nom si elles ne sont pas dans le même espace. La directive using sert à définir des alias pour rendre plus facile l’identification des espaces de noms ou des classes. Elle permet aussi d’accéder à des types sans avoir à préciser à chaque fois l’espace auquel elles appartiennent.
19
=Labat FM.book Page 20 Vendredi, 19. juin 2009 4:01 16
20
Développement XNA pour la XBox et le PC
Analysons le programme suivant : using System; namespace PremierProgramme { class Program { static void Main(string[] args) { Console.WriteLine(Math.PI); Console.ReadLine(); } } }
La classe Math et la classe Console sont contenues dans l’espace de noms System. Si nous supprimions la ligne qui contient la directive using, le programme ne pourrait plus être compilé puisqu’il ne saurait plus à quoi correspondent les noms Math et Console. Il faudrait donc le récrire de la manière suivante : System.Console.WriteLine(System.Math.PI); System.Console.ReadLine();
Le code ci-dessous présente la directive using dans la définition d’alias. using A = System.Console; using B = System.Math; namespace PremierProgramme { class Program { static void Main(string[] args) { A.WriteLine(B.PI); A.ReadLine(); } } }
Créer une classe Il est temps à présent d’écrire une première classe. Cliquez sur Projet, puis sur Ajouter une classe. Nommez le fichier Humain.cs (figure 1-8).
=Labat FM.book Page 21 Vendredi, 19. juin 2009 4:01 16
Débuter en C# CHAPITRE 1
Figure 1-8
Création d’une nouvelle classe
Voici le code de base généré par Visual Studio : using using using using
System; System.Collections.Generic; System.Linq; System.Text;
namespace PremierProgramme { class Humain { } }
Nous reconnaissons la syntaxe de déclaration d’une classe. Vous notez également que toute classe doit être contenue dans un espace de noms. Ici, l’espace de noms correspond au nom de notre projet. L’une des choses les plus importantes dans la création d’une classe est la notion d’encapsulation. Derrière ce terme, se cache la notion de droits d’accès aux éléments d’une classe. Tableau 1-8 Visibilité d’un élément Droit
Description
Public
Accessible depuis l’extérieur de l’objet
Private
Accès restreint à l’objet
21
=Labat FM.book Page 22 Vendredi, 19. juin 2009 4:01 16
22
Développement XNA pour la XBox et le PC
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
set { variable = value; } }
Mais, pour appliquer ce concept à notre cas d’étude, il faut rester logique. En effet, implémenter l’opérateur d’écriture n’est peut être pas nécessaire puisqu’il est très rare qu’un humain puisse changer de nom. public string Nom { get { return nom; } }
Dernière chose à faire, le nom du personnage doit lui être attribué à sa création. Il faut donc ajouter un argument au constructeur et initialiser le champ. public Humain(string nom) { this.nom = nom; }
Un problème se pose ici. En effet, le nom de l’argument attendu par le constructeur (nom) est le même que le nom du champ à initialiser. Dans ce cas, l’usage du mot-clé this permet alors de désigner l’instance courante de la classe, et ainsi accéder au champ nom et pas à l’attribut du constructeur. Si, à ce stade, vous essayez de compiler le programme, vous obtiendrez une erreur : 'PremierProgramme.Humain' ne contient pas de constructeur qui accepte des arguments ➥ '0'
En effet, votre constructeur attend un argument. Or vous ne lui en passez aucun lors de la création de votre objet. Retournez donc dans le fichier Program.cs et modifiez la ligne comme bon vous semble. Humain heros = new Humain("Moi");
Vous pouvez afficher le nom de notre humain en utilisant la propriété que vous avez définie plus tôt : Console.WriteLine(heros.Nom);
Pour finir, écrivez une méthode appelée « SePresenter » à la classe Humain. Elle a pour fonction d’afficher une petite phrase de présentation dans la console. Puis, utilisez-la dans la fonction Main. Voici ci-dessous le code source final de votre classe. Humain.cs namespace PremierProgramme { class Humain { string nom; public string Nom {
23
=Labat FM.book Page 24 Vendredi, 19. juin 2009 4:01 16
24
Développement XNA pour la XBox et le PC
get { return nom; } } public Humain(string nom) { this.nom = nom; } public void SePresenter() { System.Console.WriteLine("Bonjour, je m'appelle " + nom); } } } En pratique L’opérateur + sert à concaténer plusieurs chaînes de caractères.
Program.cs using System; namespace PremierProgramme { class Program { static void Main(string[] args) { Humain heros = new Humain("Moi"); heros.SePresenter(); Console.ReadLine(); } } }
En résumé Vous disposez à présent du bagage de connaissances nécessaires à la compréhension des bases de XNA, à savoir : • choix et utilisation de types de données ; • écriture de structures conditionnelles ; • écriture et utilisation de fonctions ou de procédures ; • compréhension des bases de la programmation orientée objet, des classes et des objets. Dans le chapitre suivant, vous allez appliquer les notions que nous venons de voir, et vous ferez vos premiers pas dans l’univers de la création de jeu.
=Labat FM.book Page 25 Vendredi, 19. juin 2009 4:01 16
2 Prise en main de XNA Connaissez-vous réellement les possibilités qu’offre le framework XNA ? Ce chapitre les présente afin que vous vous rendiez compte de quoi vous serez capable après quelques heures de pratique avec XNA. Après avoir lu ce premier chapitre, vous serez en mesure de créer votre premier projet, de comprendre les différents éléments qui le composent et de le déployer sur une Xbox 360.
Télécharger l’EDI et XNA Si l’EDI Microsoft Visual C# Express 2008 et le framework ne sont pas déjà sur votre ordinateur, voici la procédure à suivre pour vous en équiper : 1. Téléchargez Microsoft Visual C# Express 2008 en vous rendant à cette adresse : http://www.microsoft.com/express/download/ Figure 2-1
Téléchargement de Microsoft Visual C# Express 2008
=Labat FM.book Page 26 Vendredi, 19. juin 2009 4:01 16
26
Développement XNA pour la XBox et le PC
2. Une fois le fichier téléchargé, exécutez-le pour démarrer l’installation. 3. Il ne reste plus qu’à télécharger et installer XNA en vous rendant sur le site officiel à cette adresse : http://creators.xna.com/en-us/xnags_islive
Partir d’un starter kit En général, les starter kits sont des projets de jeux prêts à l’emploi et à compiler. Ils sont faciles à modifier et constituent une bonne base pour vos projets : vous pouvez analyser leur code source, en utiliser une partie dans vos jeux et le modifier. Penchons-nous sur celui livré avec XNA 3.0 : 1. Ouvrez Visual Studio. 2. Cliquez sur Fichier puis Nouveau projet. 3. Dans la section XNA Game Studio 3.0, sélectionnez Platformer Starter Kit (3.0). 4. Validez.
Figure 2-2
Création d’un projet basé sur un starter kit
Dans l’explorateur de solutions situé à droite de l’écran, vous trouvez trois projets : un pour Windows, un pour la Xbox 360 et le dernier pour Zune.
=Labat FM.book Page 27 Vendredi, 19. juin 2009 4:01 16
Prise en main de XNA CHAPITRE 2
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 ÉtatsUnis 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 communauté 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
27
=Labat FM.book Page 28 Vendredi, 19. juin 2009 4:01 16
28
Développement XNA pour la XBox et le PC
être intéressant et très instructif, ne serait-ce que pour comparer vos méthodes de programmation avec celle d’un autre développeur, ou encore pour savoir comment telle ou telle partie du jeu a été réalisée.
Partager ses projets Les starter kits restent des projets assez simples et leur vocation est essentiellement didactique. La plupart d’entre eux sont basés sur des jeux en 2D. Mais rassurez-vous, il est tout à fait possible de réaliser des jeux en 3D avec XNA. Pour vous en convaincre, il vous suffit de faire un petit tour sur le site de la communauté (http://creators.xna.com/en-US/) et de vous intéresser aux projets des autres membres. En effet, sur ce site, vous pourrez parcourir le catalogue des jeux développés par des amateurs, des développeurs patentés, voire des studios indépendants. Vous pourrez également visualiser des vidéos de présentation et même en acheter certains.
Figure 2-4
Le catalogue de jeux disponible sur le site de la communauté
Nous conseillons vivement de vous y inscrire et de participer aux forums de discussion, car c’est le meilleur endroit pour obtenir de l’aide sur XNA et, d’une manière générale, sur la programmation de jeux.
=Labat FM.book Page 29 Vendredi, 19. juin 2009 4:01 16
Prise en main de XNA CHAPITRE 2
Encouragement Au moment où nous rédigeons ce livre, il n’existe pas de lieu de rassemblement pour la communauté francophone. Cependant, nous espérons que l’anglais n’est pas une barrière infranchissable pour vous. En effet, vous ne pourrez pas y échapper (même si vous réussissez à vous procurer des ouvrages en français tels que celui-ci), et il y a de fortes chances qu’à un moment ou un autre vous deviez échanger avec un interlocuteur étranger.
Vous aurez sûrement envie de partager vos projets. Ceci est intéressant à plusieurs titres : vous pourrez ainsi présenter vos créations à vos amis, mais surtout, vous récolterez par ce biais les avis et conseils des autres développeurs. Avec la sortie de XNA 3.0, la possibilité de vendre ses jeux sur le Xbox Live est apparue. Votre jeu est alors disponible sur le Xbox Live Market pour quelques centaines de points Microsoft. Les détails sont disponibles sur le site Internet de la communauté (http://creators .xna.com/). Une autre solution pour faire connaître vos talents de développeur et vous frotter aux autres afin de progresser est de participer aux concours de programmation XNA. Citons par exemple l’Imagine Cup, organisé chaque année par Microsoft et possédant une catégorie intitulée Game Development, dont les finalistes gagnent plusieurs milliers de dollars (http://imaginecup.com).
L’architecture d’un projet XNA Dans cette partie, nous allons d’abord nous intéresser aux différents éléments du framework, puis nous nous pencherons sur les différentes méthodes qui composent le cycle de vie d’un jeu vidéo sous XNA.
Structure du framework Le framework XNA comporte essentiellement trois parties, chacune correspondant à une DLL (Dynamic Link Library, c’est-à-dire une bibliothèque de fonctions) : • le moteur graphique (Microsoft.XNA.Framework.dll), qui contient tout qu’il faut pour gérer l’affichage dans votre jeu ; • le modèle d’application d’un jeu (Microsoft.XNA.Framework.Game.dll), que nous détaillerons dans la section « Structure du code » ; • et le content pipeline (Microsoft.XNA.Framework.Content.Pipeline.dll), utile à la gestion des ressources (texture, son, etc.) du jeu. Les fonctions contenues dans ces bibliothèques font appel à des fonctions de DirectX de plus bas niveau, c’est-à-dire qu’à une ligne de code utilisant le framework XNA correspondent plusieurs lignes de code utilisant directement DirectX.
29
=Labat FM.book Page 30 Vendredi, 19. juin 2009 4:01 16
30
Développement XNA pour la XBox et le PC
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 composants créés par d’autres et mettre les leurs à disposition (les composants seront abordés au chapitre 4). Nous suivrons cette recommandation dans ce livre, le modèle étant très simple à comprendre et le code créé très bien organisé de cette manière.
Créer un projet Le temps est maintenant venu de débuter notre premier projet. Dans Visual Studio cliquez sur Fichier puis sur Nouveau projet. Sélectionnez Windows Game (3.0), puis validez. Ouvrez le fichier Game1.cs. Vous reconnaissez l’architecture que nous venons de voir et, grâce aux commentaires, vous comprenez ce que fait notre programme à chaque appel de Update() et de Draw(). Dans le fichier Program.cs, vous retrouvez la fonction Main() de notre programme. C’est ici que notre objet Game1 s’instancie, et que le lancement du jeu a lieu via la méthode Run().
=Labat FM.book Page 31 Vendredi, 19. juin 2009 4:01 16
Prise en main de XNA CHAPITRE 2
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.
31
=Labat FM.book Page 32 Vendredi, 19. juin 2009 4:01 16
32
Développement XNA pour la XBox et le PC
Figure 2-6
Une première fenêtre avec XNA
En pratique Ce mécanisme s’appelle IntelliSense. Il s’agit du système d’autocomplétion de Microsoft qui, en plus de vous aider dans l’écriture du code, vous fournit de la documentation sur les classes, fonctions, etc. Son fonctionnement est repris plus en détail dans l’annexe A.
Figure 2-7
Choix d’une couleur grâce à IntelliSense
Si vous avez regardé le code de base d’un peu plus près, vous avez peut-être remarqué la présence d’un objet graphics de type GraphicsDeviceManager. C’est cet objet qui gère les traitements graphiques du jeu. Ainsi, vous pouvez facilement redimensionner votre application.
=Labat FM.book Page 33 Vendredi, 19. juin 2009 4:01 16
Prise en main de XNA CHAPITRE 2
public Game1() { graphics = new GraphicsDeviceManager(this); this.graphics.PreferredBackBufferHeight = 100; this.graphics.PreferredBackBufferWidth = 100; Content.RootDirectory = "Content"; }
Ou encore, la faire démarrer en mode plein écran. public Game1() { graphics = new GraphicsDeviceManager(this); this.graphics.ToggleFullScreen(); Content.RootDirectory = "Content"; }
S’outiller pour développer sur Xbox 360 Si vous souhaitez développer pour la Xbox 360, vous devez disposer d’un abonnement Premium au XNA Creators Club. Cet abonnement vous coûtera 49 pour 4 mois ou 99 pour un an. Si vous êtes étudiant, vous avez accès à une version d’essai de l’abonnement premium. Renseignez-vous auprès de votre structure enseignante. Il faut également configurer votre Xbox pour transférer vos jeux depuis votre PC : 1. Depuis votre console, connectez-vous à Xbox Live, puis téléchargez XNA Game Studio Connect. 2. Rendez-vous ensuite dans votre bibliothèque de jeu, puis dans la section « Jeux de la communauté » et lancez l’application que vous venez de télécharger. 3. La première fois que vous lancez cet utilitaire, vous voyez un code apparaître à l’écran, notez-le. 4. Sur votre ordinateur, lancez l’application XNA Game Studio Device Center, soit en allant la chercher dans le répertoire XNA Game Studio 3.0, soit à partir de Visual Studio (menu Outils). 5. Cliquez sur Add Device, choisissez Xbox 360, entrez le nom que vous voulez donner à votre console, puis insérez le code que vous avez récupéré auparavant. Vous pouvez maintenant déployer votre projet sur votre Xbox. Pour ce faire, assurez-vous que vous avez bien démarré XNA Game Studio Connect sur la console et compilez le jeu dans Visual Studio. Le reste se fait automatiquement et votre jeu devrait apparaître sur la console. Si vous avez créé un projet pour Windows et que vous voulez finalement le lire sur votre Xbox, il vous suffit de faire un clic droit sur le projet dans l’explorateur de solutions de
33
=Labat FM.book Page 34 Vendredi, 19. juin 2009 4:01 16
34
Développement XNA pour la XBox et le PC
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
36
Développement XNA pour la XBox et le PC
Qu’est-ce qu’un sprite ? De quoi est constitué le jeu Pong ? De deux raquettes et une balle, soit trois formes à afficher (figure 3-1). On appelle ces formes des sprites et non des images. Pourquoi ? Une image n’est qu’un tableau de pixels. Mais dans le cas de ce jeu, pour afficher une raquette, vous ne pouvez pas vous contenter de son image, il faut également lui donner une position sur l’écran : c’est le rôle du sprite. Il englobe une image et des informations relatives à son affichage. Figure 3-1
En 1972, Pong est le premier gros succès du jeu vidéo
L’intérêt de différencier les notions d’image et de sprite permet également d’économiser des ressources. Imaginez que vous deviez programmer un Pong sur un système ne permettant de stocker que deux images en mémoire. Pour pouvoir tout de même afficher trois sprites, vous devrez charger une image de raquette et une image de balle en mémoire, puis créer trois sprites en précisant à chacun d’eux l’adresse en mémoire de l’image à laquelle ils doivent être liés. Les deux sprites correspondants aux deux raquettes seront donc liés à une même image (figure 3-2). Figure 3-2
Les deux sprites utilisent la même image
Bien sûr, les contraintes sont volontairement exagérées dans cet exemple. Cependant, dans le cas d’un jeu devant afficher cent fois la même image, imaginez l’espace mémoire qui serait utilisé si elle était chargée autant de fois !
=Labat FM.book Page 37 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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).
37
=Labat FM.book Page 38 Vendredi, 19. juin 2009 4:01 16
38
Développement XNA pour la XBox et le PC
Ajouter un fichier au Content Manager se fait de la même manière qu’ajouter un fichier source à un projet : 1. Dans l’explorateur de solutions, effectuez un clic droit sur Content et choisissez Ajouter puis Élément existant (figure 3-4).
Figure 3-4
Ajout d’un objet au Content Manager
2. Allez chercher le fichier désiré puis validez. 3. Sélectionnez-le ensuite dans l’explorateur de solutions puis visualisez ses propriétés (raccourci clavier F4).
Figure 3-5
Propriétés de notre texture
Le champ Asset Name (figure 3-5) contient le nom que vous devrez spécifier pour utiliser la texture dans votre jeu. Par défaut, il s’agit du nom du fichier sans son extension. Vous pouvez également définir la manière dont sera traité le fichier par les champs Content Importer et Content Processor, mais vous n’y toucherez pas pour le moment. Fichiers XNB Lors de la compilation du projet, le Content Manager transformera notre fichier PNG en un fichier XNB (optimisé afin d’améliorer sa vitesse de chargement lors de l’exécution du jeu) qu’il placera dans le répertoire du jeu, le champ Copier dans le répertoire de sortie concerne le fichier original et non le fichier .xnb, nous conserverons donc l’option Ne pas copier.
=Labat FM.book Page 39 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
Il faut à présent stocker notre image dans un objet de type Texture2D. Pour cela, déclaronsle au début de notre classe : Texture2D balleTexture;
Puis, dans la méthode LoadContent(), chargeons notre image : balleTexture = Content.Load("balle"); Syntaxe
Content.Load(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 signification 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écanismes 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();
39
=Labat FM.book Page 40 Vendredi, 19. juin 2009 4:01 16
40
Développement XNA pour la XBox et le PC
Modulation La couleur que nous passons à la méthode Draw() sert à moduler les couleurs de notre sprite. Le fait de passer une couleur blanche équivaut à ne pas utiliser de modulation.
Syntaxe La méthode Draw() possède de nombreuses surcharges que nous étudierons au fur et à mesure de nos besoins. Voici celle que nous venons de rencontrer :
public void Draw (Texture2D texture, Vector2 position, Color color) texture : la texture du sprite position : la position du sprite à l’écran color : la couleur à utiliser pour la modulation
Appuyez ensuite sur la touche F5 pour lancer la compilation et démarrer le jeu (figure 3-6).
Figure 3-6
Affichage de notre premier sprite
Afficher plusieurs sprites Imaginons qu’à présent nous souhaitions afficher plusieurs balles à l’écran. Nous avons écrit au début de ce chapitre que, pour des questions d’optimisation, on ne charge qu’une seule fois chaque image et on y connecte les sprites qui l’utilisent. Pour mettre ce principe en œuvre, il suffit d’ajouter les composants manquants à l’affichage de notre second sprite, c’est-à-dire un second vecteur position. Vector2 ballePosition2;
Définissons maintenant les coordonnées de ce vecteur. Nous voulons que cette deuxième balle vienne se placer au milieu de l’écran.
=Labat FM.book Page 41 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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 totalement à l’écran, il faut encore lui retirer la moitié de sa largeur et de sa hauteur, soit : ballePosition2 = new Vector2(graphics.PreferredBackBufferWidth / ➥ 2 - balleTexture.Width / 2, graphics.PreferredBackBufferHeight / ➥ 2 - balleTexture.Height / 2); Attention Nous ne pouvons pas définir cette position dans la méthode Initialize(). En effet, nous utilisons la taille de balleTexture, or le chargement de balleTexture s’effectue dans la méthode LoadContent(), c’est-à-dire après la méthode Initialize(). Nous devons donc définir notre vecteur juste après le chargement de l’image.
Il ne reste plus qu’à le dessiner à l’écran (figure 3-7) : spriteBatch.Draw(balleTexture, ballePosition2, Color.White);
Figure 3-7
Affichage d’un second sprite
Si vous aviez voulu afficher autre chose qu’une seconde balle, vous auriez bien évidemment dû charger une seconde texture.
Un sprite en mouvement Votre objectif à présent est de donner du mouvement à votre premier sprite : vous allez le faire rebondir sur les bords de l’écran. Pour cela, vous devez recalculer sa position à
41
=Labat FM.book Page 42 Vendredi, 19. juin 2009 4:01 16
42
Développement XNA pour la XBox et le PC
chaque appel de la méthode Update(). Pour commencer, vous modifierez cette position de +1 pixel sur l’axe des abscisses à chaque itération. protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); ballePosition.X += 1; base.Update(gameTime); }
En démarrant le jeu, vous remarquez que votre balle se déplace vers la droite de l’écran… Malheureusement, elle sort de celui-ci. Pour la faire rebondir, vous devrez d’abord ajouter la notion de direction. Déclarez un objet Vector2 au début de votre classe : Vector2 balleDirection; Figure 3-8
Principe de direction de notre sprite à l’écran
Dans la méthode Initialize(), définissez la direction de départ. Par exemple, vers la gauche et vers le bas (figure 3-8) : balleDirection = new Vector2(-1, 1);
Puis à chaque appel à Update(), il faut déplacer votre sprite dans la direction voulue. Additionnez donc les deux vecteurs : protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); ballePosition += balleDirection; base.Update(gameTime); }
Votre sprite se déplace à présent bien vers le bas et vers la gauche, mais il sort toujours de l’écran. Il faut ajouter dans la méthode Update(), juste avant de calculer sa nouvelle position, des conditions qui vont tester s’il sort de l’écran. Si, durant son mouvement, il rencontre un bord de l’écran, inversez tout simplement la direction de l’axe concerné.
=Labat FM.book Page 43 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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 = 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.
43
=Labat FM.book Page 44 Vendredi, 19. juin 2009 4:01 16
44
Développement XNA pour la XBox et le PC
Heureusement, la méthode Update() reçoit un objet GameTime comme paramètre. Grâce à ce dernier, vous avez accès au nombre de millisecondes écoulées depuis le dernier appel à Update. Au niveau du code, voici ce que cela donne : ballePosition += (balleDirection * vitesse * gameTime.ElapsedGameTime.Milliseconds);
Voici le code source récapitulant les points vus précédemment : using using using using using using
System; System.Collections.Generic; Microsoft.Xna.Framework; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework.Graphics; Microsoft.Xna.Framework.Input;
namespace Chap3 { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Texture2D balleTexture; Vector2 ballePosition; Vector2 ballePosition2; Vector2 balleDirection; float vitesse; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); ballePosition = new Vector2(10, 20); balleDirection = new Vector2(-1, 1); vitesse = 0.2f; } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); balleTexture = Content.Load("balle"); ballePosition2 = new Vector2(graphics.PreferredBackBufferWidth / ➥ 2 - balleTexture.Width / 2 , graphics.PreferredBackBufferHeight / 2 - balleTexture.Height / 2); }
=Labat FM.book Page 45 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ➥ ButtonState.Pressed) this.Exit(); if (ballePosition.X = graphics.PreferredBackBufferHeight ➥ balleTexture.Height) balleDirection.Y *= -1; ballePosition += (balleDirection * vitesse * ➥ gameTime.ElapsedGameTime.Milliseconds); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); spriteBatch.Draw(balleTexture, ballePosition, Color.White); spriteBatch.Draw(balleTexture, ballePosition2, Color.White); spriteBatch.End(); base.Draw(gameTime); } } }
Une classe pour gérer vos sprites Pour vous faciliter la tâche lors de vos développements futurs, vous allez maintenant écrire une classe pour créer facilement des sprites. Vous continuerez à améliorer et à utiliser cette classe tout au long de ce livre.
Créer une classe Sprite Ajoutez une nouvelle classe à votre projet et nommez-la « Sprite ».
45
=Labat FM.book Page 46 Vendredi, 19. juin 2009 4:01 16
46
Développement XNA pour la XBox et le PC
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(assetName); }
Les deux méthodes suivantes seront celles appelées à chaque frame (image) du jeu. Notez qu’elles portent le même nom que celles de la structure de base de votre code. À vrai dire, la méthode Update aurait pu ne pas être implantée et les traitements s’effectuer directement sur le champ position via une propriété. Là encore, c’est une décision personnelle et le développeur est totalement libre de choisir la solution qu’il juge la plus adaptée. public void Update(Vector2 translation) { position += translation; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, Color.White); }
Enfin, voici le code source final de la classe : using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace PremierProjetXNA { class Sprite
=Labat FM.book Page 47 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
{ Vector2 position; Texture2D texture; public Sprite(Vector2 position) { this.position = position; } public Sprite(float x, float y) { position = new Vector2(x, y); } public void LoadContent(ContentManager content, string assetName) { texture = content.Load(assetName); } public void Update(Vector2 translation) { position += translation; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, Color.White); } } }
Vous serez amené à étoffer cette classe dès la fin de ce chapitre.
Utiliser la classe Sprite Votre classe étant prête, vous allez maintenant apprendre à l’utiliser. Retournez dans votre fichier Game1.cs et ajoutez un champ correspondant à un personnage que vous voulez afficher à l’écran. Sprite personnage;
Évidemment, l’initialisation de la variable se fait dans la méthode Initialize. personnage = new Sprite(100, 100);
Le constructeur utilisé ici est celui qui attend deux nombres réels. La ligne suivante utilise le constructeur qui attend un vecteur. personnage = new Sprite(new Vector2(100, 100));
Libre à vous de choisir celui que vous préférez !
47
=Labat FM.book Page 48 Vendredi, 19. juin 2009 4:01 16
48
Développement XNA pour la XBox et le PC
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 retrouverez ci-dessous le code source de l’utilisation de votre classe Sprite (figure 3-10). using using using using using using using using using using using
System; System.Collections.Generic; System.Linq; Microsoft.Xna.Framework; Microsoft.Xna.Framework.Audio; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework.GamerServices; Microsoft.Xna.Framework.Graphics; Microsoft.Xna.Framework.Input; Microsoft.Xna.Framework.Media; Microsoft.Xna.Framework.Net;
=Labat FM.book Page 49 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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();
49
=Labat FM.book Page 50 Vendredi, 19. juin 2009 4:01 16
50
Développement XNA pour la XBox et le PC
personnage.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } }
Classe héritée de Sprite Il est temps d’illustrer une nouvelle facette de la programmation objet en s’intéressant au cas d’un mini RPG (Role Playing Game, jeu de rôle). Dans votre projet, ajoutez une nouvelle classe que vous baptiserez Human. Ici, l’objectif est de créer un personnage qui sera représenté à l’écran par un sprite pourvu de caractéristiques spécifiques, telles qu’un nom, un niveau d’intelligence, etc. Ainsi, il est possible de considérer un objet Human comme un objet Sprite davantage spécialisé. Notez que la même réflexion est tout à fait possible avec une automobile ou un ballon de football : il s’agit d’entités qui possèdent les caractéristiques d’un objet Sprite, mais pas seulement. Dans le monde de la programmation objet, cette notion s’appelle l’héritage. Role Playing Game Le gameplay des jeux de rôle tels que Oblivion utilise des systèmes de classes et de spécialisations, chaque classe ou spécialisation ayant des traits communs mais aussi des traits plus particuliers. L’héritage est donc tout à fait approprié pour modéliser ce comportement.
En C#, la déclaration d’une classe qui hérite d’une autre se fait de la manière suivante : class Fille: Mere { }
Dans le cas étudié, il s’agit donc de : class Human: Sprite { } Héritage multiple Si vous connaissez le langage C++, vous avez sûrement entendu parler d’héritage multiple, bien qu’il ne soit que peu conseillé. En C#, cette notion n’existe pas. Cependant, l’utilisation d’interfaces constitue une autre approche du problème. Ce mécanisme du langage sera expliqué dans le chapitre 4.
Si, à ce stade, vous essayez de compiler le projet, vous obtiendrez l’erreur suivante : 'PremierProjetXNA.Sprite' ne contient pas un constructeur qui accepte des arguments ➥ '0'
=Labat FM.book Page 51 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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 constructeur 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 remplacer 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;
51
=Labat FM.book Page 52 Vendredi, 19. juin 2009 4:01 16
52
Développement XNA pour la XBox et le PC
namespace PremierProjetXNA { class Human : Sprite { string name; int health; public Human(Vector2 position, string name, int health) : base(position) { this.name = name; this.health = health; } } }
Qu’ont un mage et un guerrier en commun ? À l’origine il s’agit bien évidemment d’humains… Encore que tout amateur de jeux de rôles vous dira qu’un orque ou encore un elfe peut tout aussi bien accomplir cette tâche. Là encore, la notion d’héritage peut s’appliquer… using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace PremierProjetXNA { class Wizard : Human { int intelligence; public Wizard(Vector2 position, string name, int health, int intelligence) : base(position, name, health) { this.intelligence = intelligence; } } } using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace PremierProjetXNA { class Warrior : Human { int strength; public Warrior(Vector2 position, string name, int health, int strength) : base(position, name, health) { this.strength = strength; } } }
=Labat FM.book Page 53 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
using using using using
Microsoft.Xna.Framework; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework.Graphics; 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); }
53
=Labat FM.book Page 54 Vendredi, 19. juin 2009 4:01 16
54
Développement XNA pour la XBox et le PC
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); mage.Draw(spriteBatch); guerrier.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } }
Vous pouvez ainsi imaginer une hiérarchie de classes : celles se trouvant en haut de la pyramide sont généralistes et plus l’on descend, plus elles sont spécialisées (figure 3-11). Figure 3-11
Un diagramme de classes représentant notre exemple
Imaginez toujours un monde fantastique, dans lequel personne n’est ordinaire ! Il est donc possible de considérer que tous les habitants de ce monde sont des mages ou des guerriers. Ainsi, la classe Human ne devrait que servir de base aux autres classes et ne plus être directement instanciable. Ce concept se traduit en C# par le mot clé abstract. using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace PremierProjetXNA { abstract class Human : Sprite { string name; int health; public Human(Vector2 position, string name, int health) : base(position) { this.name = name; this.health = health; } } }
=Labat FM.book Page 55 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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) ;
55
=Labat FM.book Page 56 Vendredi, 19. juin 2009 4:01 16
56
Développement XNA pour la XBox et le PC
Pour terminer, la boucle for est équivalente à la boucle while. Il s’agit seulement d’une forme plus condensée de son écriture. Voici l’écriture algorithmique qui lui correspond : Pour i de n a m avec un pas de k Faire Fin pour
En C# : for (int i = 0; i < 5; i++) Console.WriteLine("Hello");
Bien évidemment, si le code à executer tient sur plusieurs lignes, vous devrez écrire les accolades. Attention Les boucles infinies peuvent causer l’instabilité d’un système, notamment lorsque la condition reste toujours vraie. Faites donc toujours bien attention lorsque vous établissez des conditions.
Il existe deux dernières instructions particulières pour la gestion de vos boucles. L’instruction break permet d’en sortir. for (int i = 0; i < 10; i++) { if (i == 5) break; Console.WriteLine(i); }
Ce qui nous donne dans la console : 0 1 2 3 4
À l’inverse, l’instruction continue fait passer directement à l’itération suivante. for (int i = 0; i < 3; i++) { if (i == 1) continue; Console.WriteLine(i); }
Le résultat dans la console est : 0 2
=Labat FM.book Page 57 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
Les tableaux Les tableaux rassemblent un ensemble de variables du même type. En C#, un tableau d’entiers se déclare de la façon suivante : int[] tableau = new int[3];
Ou bien, d’une manière plus générale : Type[] tableau[]=new Type[n];
Où le nombre n correspond à la taille du tableau. Comme pour les variables, l’initialisation d’un tableau peut se faire en même temps que sa déclaration. Deux syntaxes sont disponibles : int[] tableau = new int[] { 10, 20, 30 }; int[] tableau = { 10, 20, 30 };
La lecture d’un élément du tableau se fait de la manière suivante : Console.WriteLine(tableau[2]); En pratique Le premier élément d’un tableau a l’indice 0. Ainsi, dans le tableau déclaré ci-dessus, l’élément 3 n’existe pas et, lorsque vous affichez l’élément 2, c’est le nombre 30 qui s’affiche dans la console.
Il est également possible de déclarer des tableaux à deux dimensions. int[,] tableau = { { 1, 2, 3 }, { 4, 5, 6 } }; Console.WriteLine(tableau[1,2]);
Pour vous familiariser avec l’utilisation des boucles, entraînez-vous à parcourir tous les éléments d’un tableau. Pour vous aider, les tableaux possèdent des propriétés permettant de connaître leur taille. Ainsi, dans le cas d’un tableau à une dimension, vous pouvez utiliser la propriété Length et écrire : for (int i = 0; i < tableau.Length; i++ ) { Console.WriteLine(tableau[i]); }
Si vous utilisez des tableaux multi-dimensionnels, vous accédez à la taille de chaque dimension via la méthode GetLength(i), où i est l’indice de la dimension concernée. int[,] tableau = {{1, 2, 3},{4,5,6}}; for (int i = 0; i < tableau.GetLength(0); i++ ) { string output = ""; for (int j = 0; j < tableau.GetLength(1); j++) { if (j == 0) output += tableau[i,j].ToString();
57
=Labat FM.book Page 58 Vendredi, 19. juin 2009 4:01 16
58
Développement XNA pour la XBox et le PC
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. 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 liste = new List();
=Labat FM.book Page 59 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
Vous pouvez toutefois lui indiquer sa capacité de stockage dès son initialisation. List liste = new List(5);
Une dernière surcharge du constructeur vous permet enfin d’initialiser une liste en copiant les valeurs d’une autre liste. List premiereListe = new List(); List liste = new List(premiereListe);
Pour ajouter un élément à la liste, il suffit simplement d’écrire : List liste = new List(); 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 porte bien son nom : une clé est liée à une valeur, de la même façon que dans un dictionnaire une définition est liée à un mot. Il est donc possible de représenter cette collection comme un tableau : Clé
Valeur
keyA
valueA
keyB
valueB
La déclaration d’un dictionary se fait de la façon suivante : Dictionary dico = new Dictionary();
Dans cet exemple, les clés correspondent donc à des chaînes de caractères et les valeurs à des entiers. Pour ajouter une entrée dans le dictionnaire, il suffit de procéder ainsi : dico.Add("premiere", 12); Attention Dans une collection de ce type, chaque clé doit être unique.
59
=Labat FM.book Page 60 Vendredi, 19. juin 2009 4:01 16
60
Développement XNA pour la XBox et le PC
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. Reportezvous à la documentation du framework si vous souhaitez plus de détails (http://msdn.microsoft .com/en-us/netframework/default.aspx). Il existe bien sûr beaucoup d’autres classes pour le stockage de données qui sont fournies par le framework .NET, mais ce n’est pas le but de cet ouvrage que de les lister toutes et de les étudier en détail. Pour les découvrir, parcourez les espaces de noms System .Collections et System.Collections.Generic.
Écriture du gestionnaire d’images Avant de programmer le gestionnaire, remémorez-vous l’idée qui le sous-tend : votre programme utilise des sprites, qui peuvent avoir à utiliser la même image. Vous voulez donc vous assurer que chaque image ne sera chargée qu’une seule fois en mémoire et sera partagée entre les sprites qui doivent l’utiliser.
Figure 3-12
Diagramme de séquence correspondant à notre objectif
Créez un nouveau projet sous XNA et ajoutez-lui la classe Sprite que vous avez codée plus tôt dans ce chapitre (récrivez-la ou allez chercher le fichier via l’explorateur de solutions, au choix).
=Labat FM.book Page 61 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
Si le Content Manager n’accomplissait pas déjà cette fonction, le gestionnaire aurait un réel intérêt dans les situations où il existe un grand nombre de sprites qui emploient la même image. Commencez donc par ajouter au projet l’image que vous chargerez. Le fichier GameThumbnail.png présent dans chaque projet peut très bien faire l’affaire. Dans la classe Game1, ajoutez une liste de sprites et, dans la méthode Initialize(), remplissez la liste de manière à recouvrir l’écran de sprites. List sprites = new List(); protected override void Initialize() { for (int i = 0; i < 13; i++) { for (int j = 0; j < 10; j++) { sprites.Add(new Sprite(i * 64, j * 64)); } } base.Initialize(); }
Notez que, dans cet exemple, les nombres 64 correspondent aux dimensions de l’image GameThumbnail.png. Si vous choisissez une autre image, n’oubliez pas de modifier ces dimensions. Chargez ensuite normalement vos images dans la méthode LoadContent(). Remarquez encore une fois la facilité déconcertante de la programmation avec XNA et C#. protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); foreach (Sprite sprite in sprites) sprite.LoadContent(Content, "GameThumbnail"); }
Enfin, il ne reste plus qu’à dessiner les sprites de la collection. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); foreach (Sprite sprite in sprites) sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
61
=Labat FM.book Page 62 Vendredi, 19. juin 2009 4:01 16
62
Développement XNA pour la XBox et le PC
À présent, ajoutez une nouvelle classe au projet et nommez-la TextureManager. Votre gestionnaire devra utiliser une collection pour stocker les images. En analysant le problème, on se rend rapidement compte qu’un dictionnaire est tout à fait approprié : lier le nom d’une image à l’image elle-même est une bonne solution. Ajoutez également un objet de type ContentManager que vous référencerez dans le constructeur de la classe. Ainsi, vous n’aurez pas à le repasser en argument à chaque utilisation du gestionnaire. Il ne reste plus que la méthode qui nous intéresse tout particulièrement. La figure 3-12 résume bien le comportement que vous devez programmer. Utilisez la méthode ContainsKey() du dictionnaire pour vérifier si une image a déjà été chargée ; si c’est le cas, renvoyez-la, sinon chargez-la avant de la renvoyer. using using using using
System.Collections.Generic; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework; Microsoft.Xna.Framework.Graphics;
namespace GestionnaireImage { class TextureManager { Dictionary textureBank = new Dictionary(); ContentManager content; public TextureManager(ContentManager content) { this.content = content; } public Texture2D GetTexture(string assetName) { if (textureBank.ContainsKey(assetName)) return textureBank[assetName]; else { textureBank.Add(assetName, content.Load(assetName)); return textureBank[assetName]; } } } }
Il ne reste plus qu’à utiliser le gestionnaire. Dans la définition de la classe Sprite, modifiez la méthode LoadContent(). À la place du paramètre de type ContentManager, ajoutez-en un de type TextureManager et employez la fonction que vous venez de définir. public void LoadContent(TextureManager textureManager, string assetName) { texture = textureManager.GetTexture(assetName); }
=Labat FM.book Page 63 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
Pour terminer, dans la classe Game1, déclarez votre TextureManager, initialisez-le et modifiez l’appel à LoadContent() de vos sprites. TextureManager textureManager; protected override void Initialize() { textureManager = new TextureManager(Content); // … } protected override void LoadContent() { // … foreach (Sprite sprite in sprites) sprite.LoadContent(textureManager, "GameThumbnail"); }
Vous pouvez à présent démarrer le programme. La version qui utilise le gestionnaire a le même comportement que celle qui ne l’utilise pas. Cependant, il s’agit là d’un bon entraînement aux collections et aux boucles et c’est aussi une bonne introduction à la mesure des performances de votre code.
Mesure des performances Il existe deux principaux facteurs de performances que vous pouvez mesurer : l’utilisation mémoire et le temps d’exécution.
Figure 3-13
Utilisation mémoire (les deux courbes se chevauchent)
63
=Labat FM.book Page 64 Vendredi, 19. juin 2009 4:01 16
64
Développement XNA pour la XBox et le PC
Dans le cas du gestionnaire d’images, essayez donc d’exécuter le programme tout en jetant un œil à l’onglet Processus du gestionnaire des tâches de Windows, et tout particulièrement à la colonne Utilisation mémoire. Effectuez la même expérience avec un nombre de sprites identique, en employant la même image, mais sans utiliser le gestionnaire d’images. Vous observerez que la mémoire employée dans le premier cas est légèrement supérieure à celle utilisée dans le second cas. La mesure du temps d’exécution peut se faire avec une précision de l’ordre du millième de seconde grâce à un objet Stopwatch, qui se trouve dans l’espace de noms System.Diagnostics. Ajoutez un objet de ce type au projet, démarrez le chronomètre avant le chargement des images et arrêtez-le juste après. Enfin, affichez le résultat dans le titre de la fenêtre grâce à la propriété Title de l’objet Window. Stopwatch stopWatch = new Stopwatch(); protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); stopWatch.Start(); foreach (Sprite sprite in sprites) sprite.LoadContent(textureManager, "GameThumbnail"); stopWatch.Stop(); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); this.Window.Title = stopWatch.ElapsedMilliseconds.ToString(); base.Update(gameTime); }
Charge du processeur Le nombre de sprites a volontairement été augmenté, de manière à souligner les variations de performances entre les différentes solutions techniques.
Faites ensuite la même chose pour la version du programme qui n’utilise pas le gestionnaire. La figure 3-14 compare les résultats entre les deux versions. À chaque fois, plusieurs lancements de l’application ont été effectués pour ne garder que la moyenne des valeurs obtenues. Cette fois-ci, vous constatez que la version qui emploie le gestionnaire d’images est plus performante.
=Labat FM.book Page 65 Vendredi, 19. juin 2009 4:01 16
Afficher et animer des images : les sprites CHAPITRE 3
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 optimisations 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.
65
=Labat FM.book Page 66 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 67 Vendredi, 19. juin 2009 4:01 16
4 Interactions avec le joueur Dans un jeu vidéo, l’interaction avec le joueur peut se faire via différents périphériques. Ce chapitre a pour but de vous apprendre à contrôler les périphériques utilisables avec XNA. Ce sera aussi l’occasion de découvrir un nouvel aspect de la programmation objet que vous appliquerez directement pour la gestion du clavier. Nous verrons également des exemples d’interactions avancées intéressantes à mettre en place dans vos jeux.
Utiliser les périphériques Cette première partie va vous présenter les différents périphériques compatibles avec XNA et leur utilisation. N’oubliez pas que, pour la Xbox 360, la souris et le clavier ne constituent pas des périphériques de base, vous devrez alors plutôt vous concentrer sur la gestion de la manette. En revanche, la situation est inversée si vous développez pour Windows. Toutefois, vous allez constater que les périphériques se gèrent très facilement avec XNA. Le portage de votre code d’une machine vers une autre est, de ce fait, un jeu d’enfant !
Le clavier Le clavier est un périphérique de base pour un jeu sur PC. Il est beaucoup utilisé dans les jeux de tirs ou de course. Commencez par créer un nouveau projet basé sur XNA. Importez votre classe Sprite, ajoutez une image au projet puis utilisez-la en créant un nouveau sprite.
=Labat FM.book Page 68 Vendredi, 19. juin 2009 4:01 16
68
Développement XNA pour la XBox et le PC
Figure 4-1
Récupérez facilement une classe écrite précédemment
Pense-bête Lorsque vous récupérerez une classe créée dans un précédent projet pour l’utiliser dans un nouveau, n’oubliez pas de modifier la déclaration de l’espace de noms pour pouvoir l’utiliser sans devoir ajouter de directive using au nouveau projet.
Comme les classes qui vont être utilisées ici font partie de l’espace de noms Microsoft .Xna.Framework.Input, n’oubliez pas d’ajouter une directive using, cela simplifiera l’écriture du code. Le framework met à votre disposition l’objet Keyboard qui possède la méthode GetState() et – comble de bonheur – il dispose également de la classe KeyboardState. Une fois l’état du clavier récupéré, vous aurez accès à l’ensemble des touches pressées au moment de l’appel à la méthode et pourrez également déterminer si une touche donnée est pressée ou non. L’ensemble des touches de votre clavier (et même certaines touches auxquelles vous n’avez jamais fait attention) sont disponibles via l’énumération Keys. Rappelons qu’une énumération est un type de données constitué d’un ensemble de constantes. Voici un exemple de déclaration d’énumération : enum CouleurDesYeux { bleu, marron, vert, gris };
L’accès à une valeur de l’énumération se fait en écrivant le nom de l’énumération suivi d’un point et de la valeur voulue : CouleurDesYeux.bleu;
=Labat FM.book Page 69 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
Dans l’exemple suivant, l’utilisateur a maintenant la possibilité de quitter le jeu en appuyant sur la touche Échap de son clavier (à laquelle nous accédons via Keys.Escape) : protected override void Update(GameTime gameTime) { KeyboardState KState = Keyboard.GetState(); if (KState.IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime); }
Vous pouvez également écrire une forme plus contractée, qui n’utilise pas de variable pour stocker l’état du clavier. Il faut alors faire intervenir l’objet Keyboard comme suit : if (Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit();
Voyons à présent comment déplacer notre sprite. Ajoutons une variable qui contiendra sa vitesse de déplacement. float speed = 0.1f;
Le reste est très simple. Selon la touche fléchée sur laquelle il appuie, l’utilisateur déplace le sprite dans la direction qu’il souhaite, tout en modulant cette translation par le temps qui s’est écoulé depuis la dernière frame, comme vous l’avez vu au chapitre 3. protected override void Update(GameTime gameTime) { KeyboardState KState = Keyboard.GetState(); if (KState.IsKeyDown(Keys.Left)) sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed, 0)); if (KState.IsKeyDown(Keys.Right)) sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds * speed, ➥ 0)); if (KState.IsKeyDown(Keys.Up)) sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); if (KState.IsKeyDown(Keys.Down)) sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); base.Update(gameTime); }
Pour traiter une combinaison de touches, utilisez la méthode GetPressedKeys() qui renvoie un tableau de Keys. L’exemple suivant affiche la liste des touches sur lesquelles le joueur a appuyé à la place du titre de la fenêtre.
69
=Labat FM.book Page 70 Vendredi, 19. juin 2009 4:01 16
70
Développement XNA pour la XBox et le PC
Figure 4-2
Le joueur peut maintenant déplacer son sprite où bon lui semble.
KeyboardState KState = Keyboard.GetState(); this.Window.Title = ""; foreach (Keys key in KState.GetPressedKeys()) this.Window.Title += key.ToString();
La souris Il est temps à présent d’étudier la souris. Ce périphérique est particulièrement adapté aux jeux de tir et de gestion. Cette fois-ci encore, vous pouvez utiliser un objet MouseState de manière à récupérer l’état de la souris mis à disposition par l’objet Mouse. De la même manière que pour le clavier, vous pouvez grâce à cet objet connaître l’état de la souris : les boutons utilisés, la position de la souris ou encore le nombre d’interventions sur la molette. Dans un premier temps, il faut modifier votre classe Sprite. Jusqu’à présent, vous ne pouviez qu’appliquer des translations à votre Sprite or, dans l’exemple suivant, vous souhaitez pouvoir modifier directement sa position. Ajoutez donc une propriété en lecture et en écriture concernant la position de votre sprite. Vector2 position; public Vector2 Position { get { return position; } set { position = value; } }
=Labat FM.book Page 71 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
À présent, vous avez tous les outils en main pour transformer votre sprite en curseur. À chaque appel de la méthode Update(), il suffit de remplacer la position du sprite par celle de la souris. protected override void Update(GameTime gameTime) { MouseState MState = Mouse.GetState(); sprite.Position = new Vector2(MState.X, MState.Y); base.Update(gameTime); }
Notez que, comme pour le clavier, vous n’êtes pas obligé de stocker l’état de la souris et pouvez l’exploiter directement. sprite.Position = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
En ce qui concerne les clics de la souris, vous les détectez en utilisant l’énumération ButtonState qui prend deux états : Pressed ou Released. if (MState.LeftButton == ButtonState.Pressed) this.Window.Title = "Gauche"; if (MState.MiddleButton == ButtonState.Pressed) this.Window.Title = "Milieu"; if (MState.RightButton == ButtonState.Pressed) this.Window.Title = "Droit";
Pour finir, vous pouvez récupérer les mouvements qu’effectue l’utilisateur avec sa molette via la propriété ScrollWheelValue. Celle-ci retourne une valeur entière qui s’incrémentera si l’utilisateur fait tourner la molette vers le haut et qui se décrémentera dans le cas contraire. this.Window.Title = MState.ScrollWheelValue.ToString();
La manette de la Xbox 360 La manette de la Xbox 360 est le contrôleur de base que tous les joueurs de la console possèdent. Si vous développez un jeu pour la console, vous devrez donc adapter son fonctionnement à cette manette. Le fonctionnement de la manette est similaire à celui du clavier et de la souris. Vous stockez son état dans un objet de type GamePadState que vous récupérerez de l’objet GamePad. Cependant, comme il peut y avoir plusieurs manettes connectées à la console, il faut spécifier celle qui vous intéresse grâce à l’énumération PlayerIndex. GamePadState GPState = GamePad.GetState(PlayerIndex.One);
Tout d’abord, vous pouvez vérifier qu’une manette est bien connectée à la console en utilisant la propriété IsConnected. GamePadState GPState = GamePad.GetState(PlayerIndex.Two); if (GPState.IsConnected) this.Window.Title = "La manette 2 n’est pas connectée à la console";
71
=Labat FM.book Page 72 Vendredi, 19. juin 2009 4:01 16
72
Développement XNA pour la XBox et le PC
Vous avez accès à deux méthodes classiques, l’une permettant de savoir si un bouton est pressé, l’autre s’il ne l’est pas. Le choix du bouton se fait grâce à l’énumération Buttons, laquelle vous permet également d’accéder à l’intégralité des boutons d’une manette Xbox 360. if (GPState.IsButtonDown(Buttons.A)) this.Exit(); if (GPState.IsButtonUp(Buttons.B)) this.Window.Title = "Le bouton B n’est pas pressé";
Comme d’habitude, il est tout à fait possible de s’affranchir du stockage de l’état de la manette. if (GamePad.GetState(PlayerIndex.One).IsButtonDown(Buttons.A)) this.Exit();
Vous pourriez également estimer l’état d’un bouton avec l’énumération ButtonState. Un bouton a soit l’état Pressed, soit l’état Released. if (GPState.Buttons.A == ButtonState.Pressed) this.Exit();
De la même manière, vous pourrez récupérer l’état du pad directionnel. Le cas des diagonales est géré, il peut donc y avoir deux directions pressées. if (GPState.DPad.Down == ButtonState.Pressed) Window.Title = "Vers le haut";
L’état des gâchettes analogiques gauche et droite peut aussi être récupéré. La propriété renverra un float, compris entre 0 et 1, 1 signifiant que la gâchette est complètement enfoncée. Cette variation de valeur pour être utilisée, par exemple, pour l’accélération ou la décélération dans un jeu de course. this.Window.Title = GPState.Triggers.Left.ToString();
En ce qui concerne les sticks analogiques, il est possible de récupérer un objet de type Vector2 correspondant à la distance qui les sépare de la position initiale des sticks. this.Window.Title = GPState.ThumbSticks.Left.X + " ; " + GPState.ThumbSticks.Left.Y;
Enfin, la fonction SetVibration de l’objet GamePad permet de faire vibrer la manette. Elle attend comme paramètres la manette concernée, la vitesse à appliquer au moteur gauche et celle à appliquer au moteur droit. Notez également que la fonction renvoie un booléen vous indiquant si les vibrations ont eu lieu. Lorsque vous souhaitez arrêter les vibrations, il vous suffira de passer une vitesse nulle en paramètre de la fonction. L’exemple suivant fait vibrer la manette durant 5 secondes. int time = 0; protected override void Update(GameTime gameTime) {
=Labat FM.book Page 73 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
time += gameTime.ElapsedGameTime.Milliseconds; if (time < 5000) { if (!GamePad.SetVibration(PlayerIndex.One, 1, 1)) this.Window.Title = "Impossible de faire vibrer la manette"; } else this.Window.Title = "Temps écoulé"; base.Update(gameTime); }
Vous ne le savez peut être pas mais la manette de la Xbox 360 peut également fonctionner sur votre ordinateur ; il est possible d’en brancher jusqu’à quatre et bien entendu de les utiliser dans XNA. Il faut pour cela utiliser un module USB que vous connecterez à votre ordinateur.
Utilisation de périphériques spécialisés La création d’un bon jeu passe par la définition d’un gameplay innovant. L’utilisation de périphériques sortant de l’ordinaire permet d’accéder à cette phase d’innovation, nous en donnons pour preuve les jeux de rythmes musicaux qui rencontrent un grand succès depuis quelques années. En ce qui concerne la Xbox 360, les périphériques spéciaux – qu’il s’agisse d’une guitare, d’une batterie, d’un tapis de danse ou autre – agissent comme une manette sur la console. C’est-à-dire qu’une guitare d’un jeu musical peut très bien faire office d’arme dans un jeu de tir par exemple. Ce n’est donc pas plus dur de développer un jeu prévu pour utiliser un de ces périphériques ! Vous devez toutefois faire attention à certains périphériques qui n’implémentent pas tous les boutons d’une manette classique. Voici la liste (provenant de Microsoft) des composants obligatoirement présents sur un périphérique : • les boutons A, B, X et Y ; • les boutons Back, Start et Xbox Guide ; • le pad directionnel. Cela signifie donc que toutes les autres parties d’une manette de Xbox 360, notamment les gâchettes ou les sticks directionnels, ne sont pas toujours présentes sur les périphériques de la console.
Les services avec XNA La deuxième partie de ce chapitre va vous présenter ce qu’est un service dans XNA et comment l’utiliser. Vous créerez enfin un composant qui vous permettra de récupérer facilement tous vos services n’importe où dans le code de votre jeu.
73
=Labat FM.book Page 74 Vendredi, 19. juin 2009 4:01 16
74
Développement XNA pour la XBox et le PC
Les interfaces en C# Une interface contient des prototypes de méthodes et de propriétés et peut ainsi être comparée à un contrat. Une classe qui signe le contrat, c’est-à-dire qui implémente une interface, s’engage à fournir une implémentation du contenu de l’interface. Retournez dans un projet en mode console et imaginez par exemple l’interface présentée ci-dessous. Remarquez bien que la méthode Identification() n’est pas définie, il y a seulement son prototype. public interface IUseless { string Identification(); } Nom d’une interface Ce n’est pas une règle officielle, mais généralement les développeurs font précéder le nom d’une interface par un I.
Il n’est pas non plus précisé quel type de valeur est concerné : c’est un nouveau pas vers la généricité. Dans l’exemple ci-dessous, deux classes qui implémentent cette interface sont présentées. class Machine : IUseless { string serialNumber; public Machine(string serialNumber) { this.serialNumber = serialNumber; } public string Identification() { return serialNumber; } } class Human : IUseless { string name; string firstName; public Human(string name, string firstName) { this.name = name; this.firstName = firstName; } public string Identification() { return firstName + " " + name; } }
=Labat FM.book Page 75 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
La signature du contrat s’écrit donc comme un héritage (voir le chapitre 3 à ce sujet) : Classe : Interface
Cependant, à la différence de l’héritage, une classe peut implémenter plusieurs interfaces. La syntaxe serait la suivante : Classe : InterfaceA, InterfaceB
L’utilisation des classes se fait d’une manière tout à fait classique. class Program { static void Main(string[] args) { Human human = new Human("Dylan", "Bob"); Console.WriteLine(human.Identification()); Machine machine = new Machine("B563RF2"); Console.WriteLine(machine.Identification()); Console.Read(); } }
Comment utiliser les services Les services ont été conçus pour récupérer, à partir de votre classe principale (celle qui hérite de Game), un objet qui implémente une interface donnée. Leur avantage est donc de faciliter l’utilisation des méthodes d’un objet sans avoir à le passer sans cesse en paramètre et sans utiliser de variable statique. La gestion des services se fait tout simplement par la propriété Services de la classe Game. Elle correspond à un objet de type GameServiceContainer qui dispose des trois méthodes AddService(), GetService() et RemoveService(). Les services sont en fait stockés dans un objet Dictionary. Vous allez maintenant créer votre premier service qui vous servira à gérer le clavier. Dans la première partie de ce chapitre, nous avons présenté les deux possibilités qui existent pour récupérer les entrées de l’utilisateur : à savoir utiliser directement l’objet Keyboard ou alors passer par un objet KeyboardState. Or, dès que vous commencerez à travailler sur un projet conséquent, vous vous rendrez compte que l’accès répété à l’objet Keyboard peut être à l’origine de pertes de performances. En fait, vous pourriez n’y accéder qu’une fois et mettre à disposition de tout le monde l’objet KeyboardState : cela correspond parfaitement à la définition des services. L’écriture du contrat se fait très rapidement : une fonction suffit pour savoir si une touche est effectivement pressée. interface IKeyboardService { bool IsKeyDown(Keys key); }
75
=Labat FM.book Page 76 Vendredi, 19. juin 2009 4:01 16
76
Développement XNA pour la XBox et le PC
Maintenant, ajoutez une nouvelle classe au projet et nommez-la KeyboardService. Cette classe devra implémenter l’interface IKeyboardService, mais devra aussi hériter de la classe GameComponent, laquelle implémente les interfaces IGameComponent et IUpdateable. Ces interfaces imposent respectivement l’implémentation des méthodes Initialize() et Update(). La classe Game dispose justement d’une collection de GameComponent, ainsi les méthodes citées précédemment seront automatiquement appelées pour les objets de cette collection. Vous retrouverez ici la classe KeyboardService. Il n’y a rien de particulier à signaler ici : dans le constructeur, on se contente d’ajouter la classe courante à la collection de service de l’objet Game et dans la méthode Update(), on enregistre l’état du clavier. Enfin, la fonction IsKeyDown() se contentera de consulter la méthode éponyme de l’objet KBState. class KeyboardService : GameComponent, IKeyboardService { KeyboardState KBState; public KeyboardService(Game game) : base(game) { game.Services.AddService(typeof(IKeyboardService), this); } bool IsKeyDown(Keys key) { return KBState.IsKeyDown(key); } public override void Update(GameTime gameTime) { KBState = Keyboard.GetState(); base.Update(gameTime); } }
Ajoutez, dans la méthode Initialize() de votre classe Game, un nouvel objet KeyboardService à la collection de Components. this.Components.Add(new KeyboardService(this));
Dans la méthode Update(), vous allez devoir accéder à votre service. La ligne suivante sert à récupérer un service depuis la collection de la classe Game. Il est important de garder en mémoire que la collection est un dictionnaire et que les valeurs sont donc indexées par type : il faut préciser celui de notre service, c’est le but de l’opérateur typeof. this.Services.GetService(typeof(IKeyboardService))
Cependant, à ce stade, l’objet que vous récupérez n’est toujours pas du type IKeyboardService, vous allez donc devoir le convertir. Cette opération s’appelle le casting : en précisant le
=Labat FM.book Page 77 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
type voulu entre parenthèses juste avant l’objet, vous pourrez accéder aux méthodes de l’objet. Le casting Lorsqu’il est nécessaire de convertir une donnée dans un type différent de celui choisi par la conversion automatique, il faut préciser de manière explicite le nouveau type : cette opération s’appelle le casting. Attention, cela peut parfois être source d’une perte de données. Si vous convertissez, par exemple, un nombre décimal de type float vers un int, vous perdrez la partie décimale du nombre.
((IKeyboardService)this.Services.GetService(typeof(IKeyboardService))) ➥ .IsKeyDown(Keys.Up)
Il est à présent possible de réécrire la méthode du début de ce chapitre. protected override void Update(GameTime gameTime) { if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService))) ➥ .IsKeyDown(Keys.Up)) sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService))) ➥ .IsKeyDown(Keys.Down)) sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService))) ➥ .IsKeyDown(Keys.Left)) sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed, 0)); if (((IKeyboardService)this.Services.GetService(typeof(IKeyboardService))) ➥ .IsKeyDown(Keys.Right)) sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed, 0)); base.Update(gameTime); }
Enfin, notez qu’il n’est pas obligatoire d’écrire une interface pour un service. Il est possible de procéder directement ainsi : game.Services.AddService(typeof(KeyboardService), new KeyboardService());
Les méthodes génériques Comme vous venez de le constater, récupérer un service pour l’utiliser peut être ennuyeux à la longue. Vous allez maintenant apprendre à créer une classe qui vous en simplifiera l’utilisation.
77
=Labat FM.book Page 78 Vendredi, 19. juin 2009 4:01 16
78
Développement XNA pour la XBox et le PC
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(assetName);
La déclaration d’une telle classe se fait de la manière suivante : public static void Sample(T param) { //… }
Vous pouvez également ajouter des conditions sur le type des paramètres. Le tableau 4-1 en dresse une liste : Tableau 4-1 Différentes conditions possibles sur le type des paramètres Contrainte
Description
where T : struct
Le type T doit être un type valeur.
where T : class
Le type T doit être un type référence.
where T : new()
Le type T doit disposer d’un constructeur public sans paramètres.
where T :
Le type T doit être dérivé de la classe spécifiée.
where T :
Le type T doit implémenter l’interface spécifiée.
Ajoutez une nouvelle classe au projet et nommez-la ServiceHelper. Elle fera office de classe statique et contiendra un champ statique qui sera utilisé pour stocker une référence vers votre classe Game. static class ServiceHelper { static Game game; public static Game Game { set { game = value; } } }
Ajoutez une méthode statique et générique : elle devra ensuite ajouter à la collection de services l’objet qu’elle a reçu en paramètre. public static void Add(T service) where T : class { game.Services.AddService(typeof(T), service); }
=Labat FM.book Page 79 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
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() where T : class { return game.Services.GetService(typeof(T)) as T; }
Ci-dessous, le code complet de la classe. static class ServiceHelper { static Game game; public static Game Game { set { game = value; } } public static void Add(T service) where T : class { game.Services.AddService(typeof(T), service); } public static T Get() where T : class { return game.Services.GetService(typeof(T)) as T; } }
Vous devez maintenant modifier le constructeur de la classe KeyboardService pour qu’il passe par la classe ServiceHelper, plutôt qu’ajouter directement le service à la collection de l’objet Game. public KeyboardService(Game game) : base(game) { ServiceHelper.Add(this); }
Pour rendre cette classe utilitaire opérationnelle, il vous reste à définir la référence vers votre classe Game. public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); }
79
=Labat FM.book Page 80 Vendredi, 19. juin 2009 4:01 16
80
Développement XNA pour la XBox et le PC
À 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().IsKeyDown(Keys.Up)) sprite.Update(new Vector2(0, -1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); if (ServiceHelper.Get().IsKeyDown(Keys.Down)) sprite.Update(new Vector2(0, 1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed)); if (ServiceHelper.Get().IsKeyDown(Keys.Left)) sprite.Update(new Vector2(-1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed, 0)); if (ServiceHelper.Get().IsKeyDown(Keys.Right)) sprite.Update(new Vector2(1 * gameTime.ElapsedGameTime.Milliseconds * ➥ speed, 0)); base.Update(gameTime); }
Toujours plus d’interactions grâce à la GUI Pour conclure, sachez que les interactions avec le joueur ne passent pas seulement par les périphériques, mais également par le gameplay et les mécanismes de jeux que vous pouvez implémenter. L’un des points les plus importants dans un jeu réside dans la communication avec le joueur, qui peut s’exercer par l’intermédiaire d’une GUI (Graphical User Interface).
Graphical User Interface Les GUI ont été présentes dès les débuts du jeu vidéo, en effet ceux-ci on toujours eu besoin de donner des indications aux joueurs. Mais ces dernières ont fortement évolué depuis Pong, leur aspect graphique en faisant parfois de véritables petites œuvres d’art. Aujourd’hui la tendance est plutôt de limiter fortement le nombre d’informations affichées à l’écran, afin d’augmenter le sentiment d’immersion (par exemple dans les jeux Gears of War ou Mirror’s Edge). Cependant, ce comportement n’est adapté qu’à certains types de jeux (jeux à la première personne notamment). Sauf gameplay particulier, il est difficile d’imaginer un jeu de gestion qui ne présente aucune information au joueur.
=Labat FM.book Page 81 Vendredi, 19. juin 2009 4:01 16
Interactions avec le joueur CHAPITRE 4
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éressante (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.
81
=Labat FM.book Page 82 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 83 Vendredi, 19. juin 2009 4:01 16
5 Cas pratique : programmer un Pong Vous disposez à présent des compétences nécessaires pour vous lancer dans la création d’un premier vrai petit jeu. Dans ce chapitre nous allons donc récrire le jeu mythique qu’est devenu Pong. Passage obligé du développeur amateur, ce projet a pour but de vous faire découvrir les pratiques en amont du développement d’un jeu vidéo, puis mettra directement en application toutes les notions découvertes précédemment.
Avant de se lancer dans l’écriture du code Avant toute chose, et ceci est valable même pour le jeu le plus basique qui soit, vous devez tout planifier. La plupart des projets, dans le monde du jeu vidéo ou ailleurs, échouent parce que cette phase de préparation a été négligée : certains arrivent à terme, mais fournissent un résultat différent de celui attendu, d’autres n’ont même pas cette chance. Même si l’exemple de ce chapitre a l’air simpliste, il vous permettra d’apprendre à prendre les choses en mains dès le début et de gagner du temps pour l’étape de développement.
Définir le principe du jeu Pong est un jeu inspiré du tennis de table qui a fait son apparition dans les années 1970, d’abord sur une borne d’arcade puis en console de salon. Il existe certainement des milliers de versions de Pong. Comme nous l’avons précisé dans l’introduction de ce chapitre, il s’agit du premier jeu que la plupart des développeurs réalisent. L’engouement que les programmeurs ont pour ce jeu contribue sans cesse à son
=Labat FM.book Page 84 Vendredi, 19. juin 2009 4:01 16
84
Développement XNA pour la XBox et le PC
renouvellement : on peut même trouver des versions massivement multi-joueur de Pong sur l’Internet. Le principe que nous allons implémenter est simple : il y a deux joueurs, chacun contrôlant une raquette. Les deux joueurs s’envoient une balle qui rebondit sur les bords haut et bas de l’écran. Si la balle touche la raquette d’un des deux joueurs, elle repart vers l’autre joueur, cependant, si elle manque la raquette, l’autre joueur gagne. La version originale de Pong était plus complète puisqu’elle possédait son et affichage du score : deux facettes d’XNA que nous n’avons pas encore abordées.
Formaliser en pseudo-code Maintenant que le principe de jeu est clair, il est possible de traduire le déroulement d’une partie en pseudo-code. Ce cycle se répétant tant que le joueur n’aura pas quitté le jeu. Tant que le joueur n’a pas lancé la partie Attendre Fin tant que Tant que la balle ne sort pas par la gauche ou la droite de l’écran Si la balle touche le haut ou le bas de l’écran Faire rebondir la balle Fin si Si la balle touche une raquette Faire rebondir la balle Fin si Fin Tant que
La figure 5-1 représente les classes bat (raquette) et ball (balle) ainsi que leurs différents champs. Ces deux classes héritent de la classe Sprite. Figure 5-1
Diagramme des classes bat et ball
=Labat FM.book Page 85 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5
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.
85
=Labat FM.book Page 86 Vendredi, 19. juin 2009 4:01 16
86
Développement XNA pour la XBox et le PC
Figure 5-3
Vous récupérerez souvent des éléments développés précédemment pour vos nouveaux projets
L’arrière-plan Ajoutez une nouvelle classe au projet que vous nommerez Background et qui dérivera de DrawableGameComponent. class Background : DrawableGameComponent { public Background(Game game) : base(game) { } public override void Initialize() { base.Initialize(); } protected override void LoadContent() { base.LoadContent(); } public override void Draw(GameTime gameTime) { base.Draw(gameTime); } }
Pour pouvoir dessiner l’arrière-plan, vous aurez besoin d’un SpriteBatch puisque vous ne disposez pas de celui présent dans la classe Pong. Ajoutez donc un champ de ce type que vous initialiserez dans la méthode Initialize(). Vous pouvez récupérer l’objet GraphicsDevice de la classe Pong en utilisant la propriété Game que met à disposition la classe parente.
=Labat FM.book Page 87 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5
SpriteBatch spriteBatch; public override void Initialize() { spriteBatch = new SpriteBatch(Game.GraphicsDevice); base.Initialize(); }
Il vous faudra finalement un sprite qui correspondra à l’image de l’arrière-plan. Déclarez un champ de type Sprite, chargez l’image voulue pour l’arrière-plan et dessinez-le. Notez que pour le chargement de l’image, vous pouvez récupérer le ContentManager de la classe Pong via la propriété Game. Voici donc le code final de la classe Background. class Background : DrawableGameComponent { Sprite sprite; SpriteBatch spriteBatch; public Background(Game game) : base(game) { } public override void Initialize() { sprite = new Sprite(new Vector2(0, 0)); spriteBatch = new SpriteBatch(Game.GraphicsDevice); base.Initialize(); } protected override void LoadContent() { sprite.LoadContent(Game.Content, "back"); base.LoadContent(); } public override void Draw(GameTime gameTime) { spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
Puis, dans le constructeur de la classe Pong, ajoutez un nouvel objet de type Background à la collection Components.
87
=Labat FM.book Page 88 Vendredi, 19. juin 2009 4:01 16
88
Développement XNA pour la XBox et le PC
public Pong() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); Components.Add(new Background(this)); }
Les raquettes Il est temps d’écrire le code de la classe Bat. Celle-ci dérivera bien sûr de la classe Sprite. Implémentez dès maintenant les champs présentés dans le diagramme de classes de la première partie de ce chapitre. class Bat : Sprite { float speed; int maxHeight; Keys keyUp; Keys keyDown; public Bat(Vector2 position, int playerNumber, int maxHeight, Keys keyUp, Keys ➥ keyDown) : base(position) { this.playerNumber = playerNumber; this.maxHeight = maxHeight; this.keyUp = keyUp; this.keyDown = keyDown; speed = 0.7f; } }
Maintenant, vous devez ajouter une méthode Update() à cette classe. Passez-lui un objet GameTime en paramètre de manière à moduler la vitesse de déplacement des raquettes. Utilisez donc la classe ServiceHelper pour savoir si le joueur a appuyé sur une touche, si c’est le cas, vérifiez que la raquette n’est pas sur l’extrémité haute ou basse de l’écran avant de déplacer le sprite. Pour vérifier si la raquette est en haut ou en bas de l’écran, rappelez-vous que l’origine du repère à l’écran est située en haut à gauche (figure 5-4) et que la position d’un sprite correspond au coin supérieur gauche de l’image. Dans les calculs, il faut donc considérer que l’extrémité haute de l’écran se situe aux points de coordonnées (x ; 0) et l’extrémité basse aux points de coordonnées (y ; maxHeight – textureHeight). Voici donc le code source de la classe Bat.
=Labat FM.book Page 89 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5
class Bat : Sprite { float speed; int maxHeight; Keys keyUp; Keys keyDown; public Bat(Vector2 position, int playerNumber, int maxHeight, Keys keyUp, Keys ➥ keyDown) : base(position) { this.maxHeight = maxHeight; this.keyUp = keyUp; this.keyDown = keyDown; speed = 0.7f; } public void Update(GameTime gameTime) { if (ServiceHelper.Get().IsKeyDown(keyDown)) if (Position.Y < (maxHeight - Texture.Height)) Position = new Vector2(Position.X, Position.Y + speed * ➥ gameTime.ElapsedGameTime.Milliseconds); if (ServiceHelper.Get().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));
89
=Labat FM.book Page 90 Vendredi, 19. juin 2009 4:01 16
90
Développement XNA pour la XBox et le PC
Components.Add(new Background(this)); playerOne = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight, ➥ Keys.A, Keys.Q); playerTwo = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight, ➥ Keys.Up, Keys.Down); } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); playerOne.LoadContent(Content, "bat"); playerOne.Position = new Vector2(20, graphics.PreferredBackBufferHeight / ➥ 2 - playerOne.Texture.Height / 2); playerTwo.LoadContent(Content, "bat"); playerTwo.Position = new Vector2(770, graphics.PreferredBackBufferHeight / ➥ 2 - playerTwo.Texture.Height / 2); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { playerOne.Update(gameTime); playerTwo.Update(gameTime); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); spriteBatch.Begin(); playerOne.Draw(spriteBatch); playerTwo.Draw(spriteBatch); spriteBatch.End(); } }
Vous devez également modifier la classe Sprite et créer une propriété en lecture pour l’objet texture, sans quoi vous ne pourrez pas récupérer ses dimensions.
=Labat FM.book Page 91 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5
public Texture2D Texture { get { return texture; } }
La balle La classe Ball dérive également de la classe Sprite. Cette fois encore, implémentez les champs présentés dans le diagramme du début de ce chapitre. class Ball : Sprite { float speed; Vector2 angle; int maxHeight; int maxWidth; public Ball(Vector2 position, int maxWidth, int maxHeight) : base(position) { this.maxHeight = maxHeight; this.maxWidth = maxWidth; speed = 0.2f; angle = new Vector2(1, 1); } }
Écrivons maintenant la méthode Update(). Celle-ci prendra en paramètre les raquettes des deux joueurs, ainsi qu’une référence vers un booléen qui détermine si le jeu est en pause ou non. En premier lieu, calculez la nouvelle position de la balle en fonction de la vitesse, du temps et de la direction. Position = new Vector2(Position.X + speed * angle.X * ➥ gameTime.ElapsedGameTime.Milliseconds, Position.Y + speed * angle.Y * ➥ gameTime.ElapsedGameTime.Milliseconds);
Récupérer les raquettes des joueurs vous sera utile pour construire des objets Rectangle. Ces objets disposent d’une méthode Intersects() vous permettant de détecter les collisions entre la balle et les raquettes. Un objet Rectangle se construit tout simplement à partir de deux coordonnées x et y, une largeur et une hauteur. Rectangle ballRect = new Rectangle((int)Position.X, (int)Position.Y, Texture.Width, ➥ Texture.Height); Rectangle playerOneRect = new Rectangle((int)playerOne.Position.X, ➥ (int)playerOne.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height); Rectangle playerTwoRect = new Rectangle((int)playerTwo.Position.X, ➥ (int)playerTwo.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
91
=Labat FM.book Page 92 Vendredi, 19. juin 2009 4:01 16
92
Développement XNA pour la XBox et le PC
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 = 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 = maxWidth) started = false; } }
=Labat FM.book Page 93 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5
De retour dans la classe Pong, déclarez un objet de type Ball, chargez sa texture et dessinezla. Ajoutez aussi un booléen et initialisez-le à false. La méthode Update() va légèrement se compliquer. Le programme devra attendre que le joueur appuie sur la barre d’espace avant de lancer la balle et rendre le mouvement des raquettes possible. Dans l’appel à la méthode Update() de la balle, vous devez passer une référence vers votre booléen. Ceci se fait grâce à l’instruction ref. public class Pong : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Bat playerOne; Bat playerTwo; Ball ball; bool started; public Pong() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); Components.Add(new Background(this)); playerOne = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight, ➥ Keys.A, Keys.Q); playerTwo = new Bat(new Vector2(0, 0), graphics.PreferredBackBufferHeight, ➥ Keys.Up, Keys.Down); ball = new Ball(new Vector2(0, 0), graphics.PreferredBackBufferWidth, ➥ graphics.PreferredBackBufferHeight); started = false; } protected override void Initialize() { Window.Title = "Appuyez sur espace pour démarrer"; base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); playerOne.LoadContent(Content, "bat"); playerOne.Position = new Vector2(20, graphics.PreferredBackBufferHeight / ➥ 2 - playerOne.Texture.Height / 2); playerTwo.LoadContent(Content, "bat");
93
=Labat FM.book Page 94 Vendredi, 19. juin 2009 4:01 16
94
Développement XNA pour la XBox et le PC
playerTwo.Position = new Vector2(770, graphics.PreferredBackBufferHeight / ➥ 2 - playerTwo.Texture.Height / 2); ball.LoadContent(Content, "ball"); ball.Position = new Vector2((graphics.PreferredBackBufferWidth / 2) ➥ (ball.Texture.Width / 2), (graphics.PreferredBackBufferHeight / 2) ➥ (ball.Texture.Height / 2)); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { base.Update(gameTime); if (started) { playerOne.Update(gameTime); playerTwo.Update(gameTime); ball.Update(gameTime, playerOne, playerTwo, ref started); } else { if (ServiceHelper.Get().IsKeyDown(Keys.Space)) { started = true; Window.Title = "Pong !"; } } } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); spriteBatch.Begin(); playerOne.Draw(spriteBatch); playerTwo.Draw(spriteBatch); ball.Draw(spriteBatch); spriteBatch.End(); } }
Votre jeu est maintenant prêt. Vous pouvez le compiler et l’essayer pendant des heures… Ou peut-être un peu moins !
=Labat FM.book Page 95 Vendredi, 19. juin 2009 4:01 16
Cas pratique : programmer un Pong CHAPITRE 5 Figure 5-4
Repère 2D utilisé pour l’affichage
Figure 5-5
Votre premier jeu n’est pas si mal, n’est ce pas ?
Améliorer l’intérêt du jeu Votre premier jeu est maintenant terminé, mais pour l’instant il n’a absolument rien d’original et est commun à tous les jeux Pong. Pourquoi ne pas essayer d’en augmenter la difficulté ? Pour commencer, augmentez la vitesse de la balle à chaque fois qu’elle entre en collision avec une raquette. public void Update(GameTime gameTime, Sprite playerOne, Sprite playerTwo, ref bool ➥ started) { Position = new Vector2(Position.X + speed * angle.X * gameTime ➥ .ElapsedGameTime.Milliseconds, Position.Y + speed * angle.Y * ➥ gameTime.ElapsedGameTime.Milliseconds); Rectangle ballRect = new Rectangle((int)Position.X, (int)Position.Y, ➥ Texture.Width, Texture.Height); Rectangle playerOneRect = new Rectangle((int)playerOne.Position.X, ➥ (int)playerOne.Position.Y, playerOne.Texture.Width, playerOne.Texture.Height);
95
=Labat FM.book Page 96 Vendredi, 19. juin 2009 4:01 16
96
Développement XNA pour la XBox et le PC
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 = maxHeight - Texture.Height) angle = new Vector2(angle.X, -1); // ... ou alors avec le joueur 1 ? else if (ballRect.Intersects(playerOneRect)) { angle = new Vector2(1, angle.Y); speed += 0.02f; } // ... ou bien le joueur 2 ? else if (ballRect.Intersects(playerTwoRect)) { angle = new Vector2(-1, angle.Y); speed += 0.02f; } // ... ou alors, l’extrémité gauche ou l’extrémité droite de l’écran a été ➥ atteinte else if (Position.X = maxWidth) started = false; }
Les possibilités d’évolution sont vraiment nombreuses (création d’une intelligence artificielle, ajout de nouvelles balles, etc.), contentez-vous de laisser libre cours à votre imagination, mais gardez toujours à l’esprit que ce n’est pas la débauche de technique qui fait qu’un jeu est bon ou non, le gameplay est vraiment important !
En résumé Dans ce chapitre, vous avez découvert que la phase de préparation (ou d’élaboration du cahier des charges) ne doit pas être oubliée. Vous avez ensuite mené à bien votre premier projet avec XNA, découvrant au passage comment créer un DrawableComponent et gérer les collisions de façon primitive.
=Labat FM.book Page 97 Vendredi, 19. juin 2009 4:01 16
6 Enrichir les sprites : textures, défilement, transformation, animation
Dans le chapitre 3, nous avons commencé notre étude des sprites en XNA. Cependant, il ne s’agissait que d’un simple aperçu des possibilités qu’offre le framework. Dans ce chapitre nous allons découvrir les spécificités concernant l’affichage des textures à l’écran et en profiterons pour améliorer notre classe Sprite. Enfin, nous découvrirons comment dessiner du texte à l’écran.
Préparation de votre environnement de travail Avant de se lancer dans le perfectionnement de vos connaissances des images dans XNA, vous devez d’abord préparer votre environnement de travail. Créez tout d’abord un nouveau projet baptisé « ChapitreSix », renommez la classe Game1 en ChapitreSix et importez la classe Sprite du chapitre 3. Pensez également à modifier l’espace de noms. Ajoutez ensuite une image au gestionnaire de contenu. Pour le début de ce chapitre, c’est l’image GameThumbnail.png, située à la racine de votre projet, qui sera utilisée. Créez alors un sprite qui utilise cette image et affichez-le. Votre projet devrait maintenant ressembler à celui visible sur la figure 6-1. Récapitulons ci-dessous le code de la classe ChapitreSix.
=Labat FM.book Page 98 Vendredi, 19. juin 2009 4:01 16
98
Développement XNA pour la XBox et le PC
Figure 6-1
Votre projet devrait ressembler à ceci
public class ChapitreSix : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Sprite sprite; public ChapitreSix() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { sprite = new Sprite(100, 100); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); sprite.LoadContent(Content, "GameThumbnail"); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
=Labat FM.book Page 99 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
La méthode Draw() de la classe SpriteBatch dispose de sept surcharges. Cependant, jusqu’à présent nous n’en avons utilisé qu’une seule… spriteBatch.Draw(texture, position, Color.White);
Rappelons, pour mémoire, que texture correspond à la texture à afficher, position à la position du coin supérieur gauche de l’image et Color.White à la teinte à appliquer à la texture, celle utilisée ici correspondant à une teinte nulle. Figure 6-2
Un sprite affiché simplement avec la classe Sprite actuelle
Texturer un objet Rectangle La surcharge de la méthode Draw() que nous savons utiliser pour le moment est classée comme étant la deuxième par Visual Studio. À quoi correspond donc la première ? Faites simplement défiler l’ensemble des surcharges avec les flèches haut et bas de votre clavier. Les paramètres attendus par cette première surcharge sont visibles sur la figure 6-3.
Figure 6-3
Le détail de la première surcharge
Seul le deuxième paramètre diffère de la surcharge que nous connaissons déjà. Là où la méthode attendait un Vector2 correspondant à la position du coin supérieur gauche de la texture, elle attend à présent un objet de type Rectangle nommé destinationRectangle. Nous avons croisé des objets de type Rectangle au chapitre précédent. Ils nous ont servi à déterminer s’il y avait collision entre les raquettes et la balle. Un rectangle comprenait en
99
=Labat FM.book Page 100 Vendredi, 19. juin 2009 4:01 16
100
Développement XNA pour la XBox et le PC
fait une paire de coordonnées X et Y, ainsi qu’une largeur et une hauteur. Nous pouvons donc facilement déduire le rôle de ce rectangle dans cette surcharge : la texture le remplira, c’est-à-dire qu’elle occupera sa position et qu’elle s’adaptera à sa taille, en se redimensionnant si nécessaire.
Modifier la classe Sprite Assez parlé, il est temps d’appliquer une texture à notre Rectangle. 1. Ajoutez un champ pour un objet de type Rectangle à votre classe Sprite ainsi qu’une propriété en lecture et écriture. Rectangle destinationRectangle; public Rectangle DestinationRectangle { get { return destinationRectangle; } set { destinationRectangle = value; } }
2. Surchargez maintenant le constructeur de la classe pour qu’il prenne un objet Rectangle en paramètre plutôt qu’une unique paire de coordonnées. public Sprite(Rectangle destinationRectangle) { this.destinationRectangle = destinationRectangle; }
3. Et enfin, modifiez la méthode Draw() pour qu’elle utilise la première surcharge. public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, destinationRectangle, Color.White); }
4. Retournez dans la classe ChapitreSix et utilisez maintenant votre nouveau constructeur, puis exécutez le programme. protected override void Initialize() { sprite = new Sprite(new Rectangle(100, 100, 300, 300)); base.Initialize(); }
Rien ne vous oblige à conserver les proportions de votre sprite ! L’exemple suivant redimensionne le sprite en modifiant sa hauteur. protected override void Initialize() { sprite = new Sprite(new Rectangle(100, 100, 300, 100)); base.Initialize(); }
=Labat FM.book Page 101 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
101
Figure 6-4
La taille de la texture est facilement modifiable
Figure 6-5
La texture est maintenant aplatie
Maintenant que nous avons fait le tour de la première surcharge, passons à la troisième. Celle-ci prend un nouveau paramètre en compte, de type Rectangle? et s’appelle sourceRectangle. Le point d’interrogation signifie que le paramètre peut être nul.
Figure 6-6
Le nouveau paramètre de la troisième surcharge
=Labat FM.book Page 102 Vendredi, 19. juin 2009 4:01 16
102
Développement XNA pour la XBox et le PC
Comme vous pouvez le voir en anglais dans la description visible sur la figure 6-6, ce rectangle permet de sélectionner la portion de la texture qui devra être dessinée. Si vous décidez de passer null plutôt qu’un objet Rectangle, c’est toute la texture qui sera dessinée. Testons cette nouvelle fonctionnalité : 1. Commencez par ajouter un nouveau champ à la classe. Il devra être de type Rectangle? et s’appeler sourceRectangle. Pensez également à ajouter des propriétés en lecture et en écriture pour cette nouvelle variable. Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } }
2. Comme d’habitude, ajoutez maintenant une nouvelle surcharge du constructeur de la classe Sprite. public Sprite(Rectangle destinationRectangle, Rectangle? sourceRectangle) { this.destinationRectangle = destinationRectangle; this.sourceRectangle = sourceRectangle; }
3. Et enfin, modifiez la méthode Draw(). public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, Color.White); }
4. La classe Sprite étant prête, il ne reste plus qu’à modifier la classe ChapitreSix qui l’utilise. Pour commencer, contentez vous de passer null comme nouveau paramètre. protected override void Initialize() { sprite = new Sprite(new Rectangle(100, 100, 64, 64), null); base.Initialize(); }
À présent, essayez d’afficher seulement le quart inférieur droit de la texture et de le redimensionner pour qu’il apparaisse dans un rectangle de 64 par 64 pixels. protected override void Initialize() { sprite = new Sprite(new Rectangle(100, 100, 64, 64), new Rectangle(32, 32, 32, ➥ 32)); base.Initialize(); }
… ou encore la moitié haute du sprite sans la redimensionner.
=Labat FM.book Page 103 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
103
protected override void Initialize() { sprite = new Sprite(new Rectangle(100, 100, 64, 32), new Rectangle(0, 0, 64, ➥ 32)); base.Initialize(); }
Figure 6-7
Ici, seule la moitié haute de la texture est affichée
Passons à la quatrième surcharge. Comme vous pouvez le constater sur la figure 6-8, elle est très similaire à la troisième surcharge, à la différence qu’elle prend un couple de coordonné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 fonctionnalité 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
104
Développement XNA pour la XBox et le PC
La classe Sprite qui prend en charge une position de type Vector2 class Sprite { Vector2 position; public Vector2 Position { get { return position; } set { position = value; } } Texture2D texture; public Texture2D Texture { get { return texture; } } Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } } public Sprite(Vector2 position) { this.position = position; } public Sprite(Vector2 position, Rectangle? sourceRectangle) { this.position = position; this.sourceRectangle = sourceRectangle; } public Sprite(float x, float y, Rectangle? sourceRectangle) { position = new Vector2(x, y); this.sourceRectangle = sourceRectangle; } public void LoadContent(ContentManager content, string assetName) { texture = content.Load(assetName); } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, Color.White); } }
=Labat FM.book Page 105 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
105
Cette nouvelle fonction de votre classe s’utilisera de la manière suivante. protected override void Initialize() { sprite = new Sprite(new Vector2(100, 100), new Rectangle(0, 0, 64, 32)); base.Initialize(); }
Pour l’instant, conservez cette version de la classe. Nous allons continuer de la faire évoluer au fur et à mesure du chapitre.
Faire défiler le décor : le scrolling Nous allons maintenant découvrir une première utilisation de la sélection d’une portion de texture. Il s’agit du scrolling. Derrière ce mot anglais se cache tout simplement la notion de défilement de l’écran dans un jeu vidéo en deux dimensions. Cette technique est utile lorsque l’intégralité du niveau ne peut être affichée sur un seul écran ; l’arrière-plan se déplace alors suivant les mouvements du joueur.
Figure 6-9
Le jeu Super Tux utilise le scrolling
1. Pour commencer, rendez-vous sur le site du développeur et MVP (Microsoft Most Valuable Professional) George Clingerman : http://www.xnadevelopment.com. Naviguez ensuite jusqu’à la catégorie sprites et récupérez l’une des images d’arrière-plan qu’il met gracieusement à la disposition des internautes. Vous pouvez aussi bien utiliser une image de votre cru si vous le désirez.
=Labat FM.book Page 106 Vendredi, 19. juin 2009 4:01 16
106
Développement XNA pour la XBox et le PC
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 coordonné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 rectangle source. Augmentez-la si l’utilisateur appuie sur la flèche droite, diminuez-la dans le cas contraire. Remarquez que cette coordonnée est en dehors de la taille réelle de la texture, son extrémité (gauche ou droite selon les cas) est répétée à l’infini. Dans le cas présent, il s’agit donc ici de la couleur bleu ciel, la même que l’arrière-plan. Une classe de test de scrolling public class ChapitreSix : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics;
=Labat FM.book Page 107 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
SpriteBatch spriteBatch; Sprite sprite; public ChapitreSix() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferWidth = 200; graphics.PreferredBackBufferHeight = 300; } protected override void Initialize() { sprite = new Sprite(new Vector2(0, 0), new Rectangle(0, 0, 200, 300)); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); sprite.LoadContent(Content, "figure_6_11"); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (ServiceHelper.Get().IsKeyDown(Keys.Right)) sprite.SourceRectangle = new Rectangle(sprite.SourceRectangle.Value.X ➥ + gameTime.ElapsedGameTime.Milliseconds / 10, 0, 200, 300); if (ServiceHelper.Get().IsKeyDown(Keys.Left)) sprite.SourceRectangle = new Rectangle(sprite.SourceRectangle.Value.X ➥ - gameTime.ElapsedGameTime.Milliseconds / 10, 0, 200, 300); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
107
=Labat FM.book Page 108 Vendredi, 19. juin 2009 4:01 16
108
Développement XNA pour la XBox et le PC
Figure 6-11
Le début d’un clone de Super Mario
Créer des animations avec les sprites sheets Avez-vous déjà entendu parler de sprite sheet (feuille de sprites) ? Peut-être pas, mais vous en avez sûrement déjà vu en fonctionnement. Il s’agit en fait d’une seule image sur laquelle figure toute une déclinaison d’un ou plusieurs sprites à différents instants t. L’ensemble des images d’un même sprite peut donc composer une animation. Il est alors facile d’imaginer l’utilité des rectangles source dans ce cas de figure : déplacer le rectangle d’un sprite à l’autre dès qu’un intervalle de temps est révolu. Commencez par récupérer une planche de sprites ou bien, si vous avez des talents de graphiste, faites-en une vous-même. Dans cette partie du chapitre, nous utiliserons à nouveau une création de George Clingerman (http://www.xnadevelopment.com).
Figure 6-12
Une planche de sprite
Notre planche de sprite fait 600 par 100 pixels et comporte six états d’un sprite. Nos rectangles source devront donc être des carrés de 100 pixels de côté. 1. Dans la classe Sprite, créez deux champs : l’un nommé index de type float et l’autre nommé maxIndex de type int. Par défaut, ces deux variables doivent être initialisées à 0. float index = 0; int maxIndex = 0;
=Labat FM.book Page 109 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
109
2. Surchargez la méthode LoadContent() pour permettre de définir un index maximum dans le cas d’une planche de sprite. public void LoadContent(ContentManager content, string assetName, int maxIndex) { texture = content.Load(assetName); this.maxIndex = maxIndex; }
3. Pour terminer, le traitement de l’animation va se faire dans la méthode Update(). À chaque fois qu’elle est appelée, stockez le nombre de millisecondes écoulées depuis le dernier appel dans la variable index. Si index à dépassé l’index maximum, fixez-le à 0. 4. Enfin, modifiez l’objet sourceRectangle en changeant la position X par la valeur entière de la variable index. Voici le nouveau code complet de la classe Sprite : La classe Sprite avec la possibilité d’utiliser une feuille de sprites class Sprite { Vector2 position; public Vector2 Position { get { return position; } set { position = value; } } Texture2D texture; public Texture2D Texture { get { return texture; } } Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } } float index = 0; int maxIndex = 0; public Sprite(Vector2 position) { this.position = position; } public Sprite(Vector2 position, Rectangle? sourceRectangle) { this.position = position;
=Labat FM.book Page 110 Vendredi, 19. juin 2009 4:01 16
110
Développement XNA pour la XBox et le PC
this.sourceRectangle = sourceRectangle; } public Sprite(float x, float y, Rectangle? sourceRectangle) { position = new Vector2(x, y); this.sourceRectangle = sourceRectangle; } public void LoadContent(ContentManager content, string assetName) { texture = content.Load(assetName); } public void LoadContent(ContentManager content, string assetName, int maxIndex) { texture = content.Load(assetName); this.maxIndex = maxIndex; } public void Update(GameTime gameTime) { if (maxIndex != 0) { index += gameTime.ElapsedGameTime.Milliseconds * 0.001f; if (index > maxIndex) index = 0; sourceRectangle = new Rectangle((int)index * sourceRectangle.Value.X, ➥ sourceRectangle.Value.Y, sourceRectangle.Value.Width, ➥ sourceRectangle.Value.Height); } } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, Color.White); } }
5. De retour dans la classe ChapitreSix, il vous reste à modifier la taille du rectangle dans l’initialisation du sprite, changer l’appel à la méthode LoadContent() et ajouter l’appel de la méthode Update(). La classe de test des feuilles de sprites public class ChapitreSix : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch;
=Labat FM.book Page 111 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
Sprite sprite; public ChapitreSix() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferWidth = 100; graphics.PreferredBackBufferHeight= 100; } protected override void Initialize() { sprite = new Sprite(new Vector2(0, 0), new Rectangle(0, 0, 100, 100)); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); sprite.LoadContent(Content, "figure_6_13", 6); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { sprite.Update(gameTime); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
111
=Labat FM.book Page 112 Vendredi, 19. juin 2009 4:01 16
112
Développement XNA pour la XBox et le PC
Varier la teinte des textures Avant d’étudier la prochaine surcharge de la méthode Draw(), il est bon de revenir sur le paramètre qui définit la teinte des textures. Jusqu’à présent, nous avons laissé ce paramètre à la valeur Color.White. Il est temps d’essayer de faire varier cette valeur et d’observer les résultats. Reprenez la classe Sprite telle qu’elle était à la fin de la section « Texturer un objet Rectangle », puis modifiez sa méthode Draw() afin de régler la teinte sur Color.Black et exécutez l’application. Le résultat est visible sur la figure 6-13. public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, Color.Black); } Figure 6-13
Avec une teinte noire, la texture semble… bien noire
Essayez la même chose mais cette fois-ci avec une teinte rouge. public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, Color.Red); }
Vous pouvez créer des effets intéressants sur votre sprite grâce aux teintes. Pour cela, ajoutez deux champs de type Color à votre classe Sprite. Le premier s’appelle color et contient la teinte courante du sprite. Le second s’appelle nextColor et, comme son nom l’indique, indique la prochaine teinte que votre sprite utilisera. L’effet que nous allons réaliser ici devra faire varier doucement la teinte du sprite d’une couleur à l’autre. Ainsi, le sprite s’illuminera puis s’assombrira. Color color = Color.Gray; Color nextColor = Color.White;
=Labat FM.book Page 113 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
113
L’évolution de la couleur de la teinte a lieu dans la méthode Update(). Chaque couleur dispose de quatre composantes : rouge, vert, bleu et alpha. La composante alpha correspond à la transparence. À chaque appel de la méthode, il faut faire évoluer chaque composante (sauf la transparence qui n’a pas d’importance ici) vers la couleur objectif : soit en l’augmentant, soit en la diminuant. Lorsque la couleur courante correspond à la couleur objectif, on modifie cette dernière. N’oubliez pas de remplacer la couleur dans l’appel à la méthode Draw() de l’objet spriteBatch. La classe Sprite prend maintenant en charge la fonction de variation de sa teinte class Sprite { Vector2 position; public Vector2 Position { get { return position; } set { position = value; } } Texture2D texture; public Texture2D Texture { get { return texture; } } Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } } Color color = Color.Gray; Color nextColor = Color.White; public Sprite(Vector2 position, Rectangle? sourceRectangle) { this.position = position; this.sourceRectangle = sourceRectangle; } public Sprite(float x, float y, Rectangle? sourceRectangle) { position = new Vector2(x, y); this.sourceRectangle = sourceRectangle; } public void LoadContent(ContentManager content, string assetName) { texture = content.Load(assetName);
=Labat FM.book Page 114 Vendredi, 19. juin 2009 4:01 16
114
Développement XNA pour la XBox et le PC
} public void Update() { if (color.R < nextColor.R) color.R++; else if(color.R > nextColor.R) color.R--; if (color.G < nextColor.G) color.G++; else if (color.G > nextColor.G) color.G--; if (color.B < nextColor.B) color.B++; else if (color.B > nextColor.B) color.B--; if (color == Color.White) nextColor = Color.Gray; else if (color == Color.Gray) nextColor = Color.White; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color); } }
Dans la classe ChapitreSix, n’oubliez pas d’ajouter l’appel à la méthode Update() de votre objet sprite. protected override void Update(GameTime gameTime) { sprite.Update(); base.Update(gameTime); }
Les possibilités d’application de cet effet sont vastes : vous pouvez l’utiliser pour les boutons de votre GUI, les menus de votre jeu ou encore un système de cycle jour/nuit dans un jeu en deux dimensions. Avant de continuer notre découverte des possibilités d’affichage des textures avec XNA, adaptez votre classe pour qu’elle soit la plus générale possible : enlevez donc la variable nextColor, ajoutez une propriété pour la variable color et surchargez le constructeur. La classe Sprite plus générique que la précédente class Sprite { Vector2 position;
=Labat FM.book Page 115 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
public Vector2 Position { get { return position; } set { position = value; } } Texture2D texture; public Texture2D Texture { get { return texture; } } Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } } Color color = Color.White; public Color Color { get { return color; } set { color = value; } } public Sprite(Vector2 position, Rectangle? sourceRectangle) { this.position = position; this.sourceRectangle = sourceRectangle; } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; } public void LoadContent(ContentManager content, string assetName) { texture = content.Load(assetName); } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color); } }
115
=Labat FM.book Page 116 Vendredi, 19. juin 2009 4:01 16
116
Développement XNA pour la XBox et le PC
Opérer des transformations sur un sprite Les trois prochaines surcharges se ressemblent fortement. La seule différence entre elles concerne la mise à l’échelle. Dans la cinquième, cela se fait via un rectangle de destination comme au début de ce chapitre, dans la sixième, cela se fait grâce à un coefficient de type float et dans la dernière, cela se fait grâce à un Vector2. Le résultat est le même dans les deux cas, c’est donc à vous de choisir celle qui convient le mieux à vos besoins.
Figure 6-14
Le détail des paramètres attendus par les trois dernières surcharges
Rotation
Tout d’abord la rotation. Celle-ci doit être exprimée en radian et s’effectue autour du point d’origine. Le point d’origine est le prochain paramètre à étudier. Pour que l’origine soit le coin supérieur gauche de l’écran, le couple de valeurs doit être 0 et 0. Bon à savoir Vous n’êtes pas obligé de créer un vecteur pour certaines valeurs particulières. En effet, il existe deux valeurs prédéfinies, l’une ayant ses composantes à 0 et l’autre ayant ses composantes à 1.
Le prochain paramètre attendu par la surcharge concerne l’échelle du sprite. Si vous lui passez une valeur en inférieure à 1, il sera rétréci, dans le cas d’une valeur égale à 1, sa taille ne sera pas modifiée et enfin, dans le cas d’une valeur supérieure à 1, il sera agrandi. Vient ensuite le tour de l’énumération SpriteEffects. Celle-ci peut prendre trois valeurs : None, FlipVertically et FlipHorizontally. None ne changera rien au rendu de votre image, FlipVertically inversera l’image en la faisant tourner de 180° autour de l’axe horizontal et, enfin, FlipHorizontally inversera l’image en la faisant tourner de 180° selon l’axe vertical. Le dernier paramètre, layerDepth, est un nombre réel compris entre 0 et 1 déterminant l’ordre de l’affichage des différents éléments. Une texture qui se voit attribuer un nombre proche de 0 sera dessinée par-dessus une texture ayant un layerDepth proche de 1. Cependant, pour que ce paramètre rentre vraiment en compte, vous devez modifier un autre élément lors de l’appel à la méthode Begin() de l’objet spriteBatch. Testons à présent ces fonctionnalités une par une. La ligne suivante utilise la septième surcharge de la fonction et dessine un sprite sans aucune modification particulière. spriteBatch.Draw(texture, position, sourceRectangle, Color.White, 0, Vector2.Zero, ➥ Vector2.One, SpriteEffects.None, 0);
=Labat FM.book Page 117 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
117
Faisons nos premiers pas dans l’utilisation des rotations. Pour l’instant ne vous préoccupez pas de l’origine. 1. Commencez par ajouter un champ de type float qui contiendra la valeur de la rotation et ajoutez également une nouvelle surcharge du constructeur qui prendra en compte cet élément. float rotation = 0; public float Rotation { get { return rotation; } set { rotation = value; } } public Sprite(Vector2 position, Rectangle? sourceRectangle, float rotation) { this.position = position; this.sourceRectangle = sourceRectangle; this.rotation = rotation; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, Color.White, rotation, ➥ Vector2.Zero, Vector2.One, SpriteEffects.None, 0); }
2. Pour tester la rotation, créez trois sprites. Le premier ne subira aucune rotation, le second une rotation de P/2, soit 90° dans le sens horaire et le dernier une rotation de – P/2, soit 90° dans le sens anti-horaire. Figure 6-15
Premier essai avec les rotations
=Labat FM.book Page 118 Vendredi, 19. juin 2009 4:01 16
118
Développement XNA pour la XBox et le PC
Sprite sprite; Sprite sprite2; Sprite sprite3; public ChapitreSix() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); sprite = new Sprite(new Vector2(100, 100), null, 0); sprite2 = new Sprite(new Vector2(100, 100), null, MathHelper.PiOver2); sprite3 = new Sprite(new Vector2(100, 100), null, -MathHelper.PiOver2); }
3. Procédez comme dans la première étape pour l’origine de la rotation. Commencez par vous occuper de la classe Sprite. Vector2 origin = Vector2.Zero; public Vector2 Origin { get { return origin; } set { origin = value; } } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float ➥ rotation, Vector2 origin) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; this.rotation = rotation; this.origin = origin; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin, ➥ Vector2.One, SpriteEffects.None, 0); }
4. Pour tester les rotations en modifiant l’origine, modifiez l’instanciation de votre deuxième sprite. En prenant le point de coordonnées (32, 32), placez l’origine au milieu de la texture : le sprite tournera donc sur lui-même. sprite2 = new Sprite(new Vector2(100, 100), null, Color.White, MathHelper.PiOver2, ➥ new Vector2(32,32));
Le résultat de ce test est visible sur la figure 6-16.
=Labat FM.book Page 119 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
119
Figure 6-16
Rotations en modifiant l’origine
Échelle
Progressons encore dans l’intégration de nouvelles fonctions à notre classe Sprite en nous occupant cette fois-ci de l’échelle. Cette fonctionnalité peut être utilisée, par exemple, pour redimensionner vos sprites en fonction de la résolution d’écran choisie par le joueur. Vector2 scale = Vector2.One; public Vector2 Scale { get { return scale; } set { scale = value; } } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float ➥ rotation, Vector2 origin, Vector2 scale) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; this.rotation = rotation; this.origin = origin; this.scale = scale; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin, ➥ scale, SpriteEffects.None, 0); }
=Labat FM.book Page 120 Vendredi, 19. juin 2009 4:01 16
120
Développement XNA pour la XBox et le PC
Cette fois-ci, vous n’avez plus besoin que d’un seul et unique sprite pour faire ce test. Dans l’exemple ci-dessous, le sprite est volontairement disproportionné. sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero, new ➥ Vector2(0.5f, 4)); Figure 6-17
Modification de l’échelle du sprite
Inversion
Procédez toujours de la même manière pour le prochain paramètre. Grâce à lui, si vous disposez d’une texture représentant la marche d’un personnage de la gauche vers la droite, vous pourrez l’inverser selon l’axe vertical et ainsi obtenir la marche de la droite vers la gauche. SpriteEffects effect = SpriteEffects.None; public SpriteEffects Effect { get { return effect; } set { effect = value; } } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float ➥ rotation, Vector2 origin, Vector2 scale, SpriteEffects effect) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; this.rotation = rotation; this.origin = origin; this.scale = scale;
=Labat FM.book Page 121 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
121
this.effect = effect; } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, origin, ➥ scale, effect, 0); }
Dans cet exemple, l’inversion se fait selon l’axe horizontal : le haut de la texture va se retrouver en bas. sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero, ➥ Vector2.One, SpriteEffects.FlipVertically);
Enfin, il ne reste plus qu’à ajouter la définition de la profondeur. C’est cette notion qui définira l’ordre d’affichage des différents sprites à l’écran. Ci-dessous, vous retrouvez le code complet de la classe Sprite qui possède maintenant de puissantes fonctions avancées. La classe Sprite qui prend en compte toutes les techniques vues dans ce chapitre class Sprite { Vector2 position; public Vector2 Position { get { return position; } set { position = value; } } Texture2D texture; public Texture2D Texture { get { return texture; } } Rectangle? sourceRectangle = null; public Rectangle? SourceRectangle { get { return sourceRectangle; } set { sourceRectangle = value; } } Color color = Color.White; public Color Color { get { return color; } set { color = value; } } float rotation = 0; public float Rotation { get { return rotation; } set { rotation = value; } }
=Labat FM.book Page 122 Vendredi, 19. juin 2009 4:01 16
122
Développement XNA pour la XBox et le PC
Vector2 origin = Vector2.Zero; public Vector2 Origin { get { return origin; } set { origin = value; } } Vector2 scale = Vector2.One; public Vector2 Scale { get { return scale; } set { scale = value; } } SpriteEffects effect = SpriteEffects.None; public SpriteEffects Effect { get { return effect; } set { effect = value; } } float layerDepth = 0; public float LayerDepth { get { return layerDepth; } set { layerDepth = value; } } public Sprite(Vector2 position) { this.position = position; } public Sprite(Vector2 position, Rectangle? sourceRectangle) { this.position = position; this.sourceRectangle = sourceRectangle; } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float ➥ rotation) { this.position = position; this.sourceRectangle = sourceRectangle; this.color = color; this.rotation = rotation; } public Sprite(Vector2 position, Rectangle? sourceRectangle, Color color, float ➥ rotation, Vector2 origin)
=Labat FM.book Page 123 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
{ 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(assetName); } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, sourceRectangle, color, rotation, ➥ origin, scale, effect, layerDepth); } }
123
=Labat FM.book Page 124 Vendredi, 19. juin 2009 4:01 16
124
Développement XNA pour la XBox et le PC
Pour tester cette dernière fonctionnalité, vous aurez besoin de deux sprites. L’un aura une valeur de profondeur de 1 et l’autre 0. sprite = new Sprite(new Vector2(100, 100), null, Color.White, 0, Vector2.Zero, ➥ Vector2.One, SpriteEffects.FlipVertically, 0); sprite2 = new Sprite(new Vector2(140, 140), null, Color.White, 0, Vector2.Zero, ➥ Vector2.One, SpriteEffects.None, 1);
Cependant, ce code n’est pas encore opérationnel. Essayez les deux versions de la méthode Draw() ci-dessous. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite.Draw(spriteBatch); sprite2.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite2.Draw(spriteBatch); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Vous constaterez que les valeurs de profondeur que vous avez fixées pour chacun des deux sprites ne sont pas appliquées. Dans la première méthode Draw(), c’est le deuxième sprite qui chevauche le premier alors que dans la seconde version de la méthode, c’est le premier qui chevauche le deuxième. Il faut paramétrer votre objet spriteBatch pour que cette fonctionnalité soit effective. Cela se fait par l’intermédiaire de la méthode Begin(). spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.BackToFront, ➥ SaveStateMode.None);
Le premier paramètre concerne la transparence. Le second, celui qui vous intéresse, indiquera l’ordre d’affichage des sprites. Il peut prendre comme valeurs SpriteSortMode .BackToFront (affichage des sprites les plus profonds d’abord) ou SpriteSortMode.FrontToBack (affichage des sprites en partant du premier plan). Enfin, le dernier paramètre permet d’enregistrer l’état du périphérique graphique, il ne sera pas utilisé ici.
=Labat FM.book Page 125 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
125
À présent, vous constatez que quel que soit l’ordre des lignes d’appel de la méthode Draw() des deux sprites, c’est le premier sprite qui est situé au-dessus du second. Figure 6-18
L’ordre d’affichage des sprites est maintenant respecté
Afficher du texte avec Spritefont En lecteur curieux, vous vous êtes certainement rendu compte que la classe SpriteBatch dispose d’une méthode DrawString(). Celle-ci prend comme paramètre un objet de type SpriteFont, le texte à afficher, un objet Vector2 correspondant à la position du coin supérieur gauche de la chaîne et, enfin, la couleur de la chaîne de caractères. Un objet SpriteFont sert à indiquer quelle texture utiliser pour dessiner du texte.
Figure 6-19
Il existe une méthode qui permet d’afficher simplement du texte
Pour créer un SpriteFont, commencez par ajouter un fichier dédié au gestionnaire de contenu (figure 6-20). Ouvrez ensuite ce nouveau fichier, qui est en fait un fichier XML. Kootenay 14 0 true
=Labat FM.book Page 126 Vendredi, 19. juin 2009 4:01 16
126
Développement XNA pour la XBox et le PC
Regular ~
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 , ainsi que la taille de la police en modifiant le nombre placé entre les balises .
=Labat FM.book Page 127 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
127
Verdana 26 10 true Bold ~
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("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
128
Développement XNA pour la XBox et le PC
spriteBatch.Begin(); spriteBatch.DrawString(font, "test", new Vector2(0, 0), Color.White); spriteBatch.End(); base.Draw(gameTime); } }
Afficher le nombre de FPS Le nombre de FPS (Frames Per Second), c’est-à-dire la quantité d’images affichées par seconde, deviendra vite une de vos plus grandes obsessions. En effet, plus ce nombre est élevé, plus une animation semble fluide. Si vous effectuez un grand nombre de calculs qui ralentissent l’apparition de chaque image, votre jeu risque de donner une impression de saccades. L’objectif de votre jeu devrait être d’avoisiner les 60 FPS. Par défaut, XNA bridera votre jeu pour qu’il ne dépasse pas cette limite. Vous pouvez considérer que 30 FPS est également acceptable, cependant, évitez de descendre en dessous de ce nombre. Pour information, au cinéma, la norme est de 24 images par secondes. La mesure du nombre de FPS permet de rapidement juger des performances de votre jeu. Cependant, cela ne pourra pas vraiment vous aider pour optimiser votre code ou comparer la vitesse de différents algorithmes. Le composant que vous allez maintenant développer vous permettra d’afficher à l’écran le nombre courant de FPS. Ajoutez une nouvelle classe au projet et faites-la dériver de DrawableGameComponent. Préparez un objet de type SpriteBatch et un autre de type SpriteFont. Il existe de nombreuses méthodes pour calculer le nombre d’images par seconde. La technique utilisée ici est celle proposée par Shawn Hargreaves sur son blog (http://blogs.msdn.com/ shawnhar/). Elle consiste à compter le nombre de passages dans la méthode Draw() puis, à chaque seconde, en déduire le nombre de FPS. La classe TimeSpan est accessible dans l’espace de noms System, n’oubliez pas de l’ajouter ! Composant d’affichage du nombre de FPS class FPSComponent : DrawableGameComponent { SpriteBatch spriteBatch; SpriteFont spriteFont; int frameRate = 0; int frameCounter = 0; TimeSpan elapsedTime = TimeSpan.Zero; public FPSComponent(Game game) : base(game) { }
=Labat FM.book Page 129 Vendredi, 19. juin 2009 4:01 16
Enrichir les sprites : textures, défilement, transformation, animation CHAPITRE 6
129
public override void Initialize() { spriteBatch = new SpriteBatch(Game.GraphicsDevice); spriteFont = Game.Content.Load("font"); } protected override void LoadContent() { } public override void Update(GameTime gameTime) { elapsedTime += gameTime.ElapsedGameTime; if (elapsedTime > TimeSpan.FromSeconds(1)) { elapsedTime -= TimeSpan.FromSeconds(1); frameRate = frameCounter; frameCounter = 0; } } public override void Draw(GameTime gameTime) { frameCounter++; spriteBatch.Begin(); spriteBatch.DrawString(spriteFont, frameRate.ToString() + " FPS", new ➥ Vector2(0, 0), Color.White); spriteBatch.End(); } }
Pour l’utiliser, vous n’avez plus qu’à l’ajouter à la liste des composants. public ChapitreSix() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); Components.Add(new FPSComponent(this));
Avant de terminer ce chapitre, un dernier détail concernant l’optimisation de la taille des polices de caractère. Pour celle chargée d’afficher le nombre de FPS, Shawn Hargreaves propose de ne spécifier dans le fichier .spritefont que les caractères qui seront utilisés. Arial 14 2 Regular
=Labat FM.book Page 130 Vendredi, 19. juin 2009 4:01 16
130
Développement XNA pour la XBox et le PC
F F P P S S 0 9
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 environnement sonore est indispensable pour donner de la profondeur et du réalisme à votre jeu et favoriser l’immersion du joueur, qu’il s’agisse de la musique ou des bruitages. Pour pousser la logique à son extrême, sachez qu’il existe même des jeux qui ne comportent que du son et aucun élément graphique ! Jusqu’à la version 2 de XNA, le son était géré par l’API XACT, directement tirée de DirectX. Depuis l’arrivée de XNA 3.0, les développeurs ont accès à une nouvelle API pour gérer le son. Cette dernière est plus simple d’utilisation (vous pouvez manier les éléments sonores comme des textures, des SpriteFont, etc.), mais ne vous offrira pas des fonctions aussi avancées que XACT. Pour que vous soyez également en mesure de comprendre le code source d’un jeu écrit pour XNA 2.0, nous couvrirons ces deux méthodes.
Travailler avec XACT Aussi appelé Microsoft Cross-Platform Audio Creation Tool, XACT est la partie du framework qui sert à créer des projets audio aussi bien pour la Xbox 360 que pour Windows. Comme nous l’avons précisé en introduction de ce chapitre, jusqu’à l’arrivée de la version 3.0 du framework, c’était la seule solution pour gérer le son dans XNA. Mais ce n’est pas parce qu’une nouvelle API est arrivée dans le framework que XACT est un mauvais outil ! Au contraire, il est inclus avec le framework pour permettre d’utiliser les mêmes pistes son et ne pas avoir à redévelopper votre jeu lors du portage entre les différentes plates-formes couvertes par XNA.
=Labat FM.book Page 132 Vendredi, 19. juin 2009 4:01 16
132
Développement XNA pour la XBox et le PC
Zune L’utilisation de XACT est impossible pour un projet de jeu Zune.
Créer un projet sonore Commencez par démarrer le logiciel, qui se trouve au même endroit que les autres utilitaires livrés avec XNA, situés par défaut dans le dossier C:\Program Files\Microsoft XNA\XNA Game Studio\v3.0\Tools, ou via le menu Démarrer : 1. Cliquez sur Démarrer. 2. Cliquez sur Tous les programmes. 3. Dans Microsoft XNA Game Studio 3.0 Tools, sélectionnez Microsoft Cross-Platform Audio Creation Tool (XACT). Figure 7-1
L’interface de XACT
L’interface de l’utilitaire se divise en quatre zones. De haut en bas, nous trouvons : • La barre des menus qui sert à gérer les projets et l’apparence du logiciel. • L’explorateur du projet grâce auquel vous inspecterez sous forme d’arborescence tous les éléments du projet. • L’explorateur de propriétés où vous pourrez modifier les paramètres de tous les éléments du projet. • La zone centrale qui, bien évidemment, vous permettra de travailler sur les éléments du projet.
=Labat FM.book Page 133 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
133
En pratique L’agencement des différentes parties de l’utilitaire est très similaire à l’agencement par défaut dans Visual Studio.
Voyons comment créer un projet sonore : 1. Créez un nouveau projet via le menu File puis New Project. La plupart des éléments de l’interface sont maintenant actifs. Notez que XACT génère deux fichiers de paramétrage du projet : un pour la Xbox 360 et l’autre pour Windows (figure 7-2). Figure 7-2
Un fichier de configuration par plate-forme
Avant de continuer, il faut connaître un peu la terminologie liée à l’organisation logique des éléments sonores d’un projet XACT. Tableau 7-1 Signification des éléments d’un projet XACT Nom
Description
Wave
Il s’agit d’un fichier audio.
Wave Bank
Il s’agit du regroupement logique dans un seul fichier de plusieurs fichiers audio.
Cue
Permet de jouer des sons.
Sound Bank
C’est le regroupement logique de plusieurs Wave Bank et de Cue.
2. Ajoutez une nouvelle banque de sons (Wave Bank). Pour cela, deux solutions s’offrent à vous : via le menu Wave Banks>New Wave Bank, ou bien par l’explorateur de projets grâce à un clic droit sur Wave Banks, puis un clic sur New Wave Bank. Vous pouvez maintenant consulter les propriétés de votre banque de sons. Vous y retrouvez le nom de la banque, une description, son type, sa taille et la méthode de compression utilisée. 3. Ensuite, ajoutez un fichier à la banque : cliquez droit dans la zone de travail puis sur Insert Wave File(s). Les formats supportés sont .wav, .aif, .aiff. Vous voyez à présent diverses informations sur les pistes, notamment un détail de la compression disponible dans l’explorateur de propriétés. Le format .wav est un format de stockage audio défini par Microsoft et IBM. Il peut contenir des données aux formats MP3, WMA, etc. Les formats .aif et .aiff sont équivalents au .wav, mais sont développés par Apple. Si vous avez de la musique dans votre jeu, déclarez-la telle quelle : déplacez-la dans la catégorie Music de l’arborescence.
=Labat FM.book Page 134 Vendredi, 19. juin 2009 4:01 16
134
Développement XNA pour la XBox et le PC
Figure 7-3
Ajout d’une banque de sons
Figure 7-4
La banque de sons contient maintenant une piste
=Labat FM.book Page 135 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
135
Écouter un son Si vous voulez écouter une piste sonore à partir de XACT, vous devrez d’abord lancer l’utilitaire XACT Auditioning Utility disponible au même endroit.
4. Créez ensuite une Sound Bank (via le menu Sound Banks, puis New Sound Bank). Dans la fenêtre qui s’ouvre alors, vous distinguez deux zones : celle du haut contient les pistes audio et celle du bas rassemble les pistes Cue correspondantes. Pour ajouter une piste à la Sound Bank, glissez-déposez la piste vers la deuxième zone.
Figure 7-5
Ajout d’une piste à la Sound Bank
5. À présent, le projet est prêt à être généré : cliquez sur File, puis sur Build, ou utilisez le raccourci clavier F7. Les fichiers générés sont disponibles dans les sous-dossiers Win ou Xbox, à l’emplacement où vous avez sauvegardé votre projet XACT.
Lire les fichiers créés Votre projet a été généré, il ne reste plus qu’à l’importer dans Visual Studio pour l’utiliser. De retour dans Visual Studio, commencez par créer un nouveau projet afin de tester les possibilités sonores de XNA. L’organisation logique que nous avons créée précédemment
=Labat FM.book Page 136 Vendredi, 19. juin 2009 4:01 16
136
Développement XNA pour la XBox et le PC
se charge aussi simplement qu’une texture. En effet, il suffit d’ajouter le fichier .xap du projet XACT dans le Content Manager, qui importe automatiquement le projet vers le jeu. Cependant, vous ne devez pas vous contenter d’ajouter ce fichier au projet, il faut aussi ajouter toutes les pistes audio à utiliser dans le projet. En pratique Dans le cas d’un projet de jeu complet, cette organisation logique est beaucoup plus complexe : vous devrez en effet gérer de nombreuses banques de sons (pour les bruitages d’un personnage ou de l’environnement, les musiques, etc.). Figure 7-6
Voila à quoi devrait ressembler le projet
Il est enfin temps de passer à la programmation d’une classe test du projet sonore. Créez des objets de type AudioEngine, SoundBank et WaveBank en chargeant les fichiers générés par XACT. Attention, vous devez spécifier le chemin complet vers ces fichiers ! N’oubliez pas non plus les extensions de fichiers. Vous pouvez ensuite lire une piste Cue simplement en utilisant la méthode PlayCue () de l’objet de type SoundBank. N’oubliez pas d’appeler auparavant la méthode Update() de l’objet AudioEngine pour le mettre à jour. Celle-ci devra également être appelée dans la méthode Update() de la classe principale. Classe test pour la lecture d’une piste Cue public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; AudioEngine engine; WaveBank waveBank; SoundBank soundBank; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; engine = new AudioEngine(@"Content\test.xgs"); soundBank = new SoundBank(engine, @"Content\Sound Bank.xsb"); waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb"); }
=Labat FM.book Page 137 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
137
protected override void Initialize() { engine.Update(); soundBank.PlayCue("Explosion1"); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { engine.Update(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Exécutez le programme. Le son est lu sans problème. Charger et lire un son est finalement aussi simple que pour les textures ! Vous auriez également pu stocker la piste dans un objet de type Cue et récupérer celui-ci grâce à la méthode GetCue du SoundBank. La piste se lit ensuite grâce à la méthode Play() : Classe test d’utilisation d’un objet de type Cue public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; AudioEngine engine; WaveBank waveBank; SoundBank soundBank; Cue sound; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; engine = new AudioEngine(@"Content\test.xgs"); soundBank = new SoundBank(engine, @"Content\Sound Bank.xsb"); waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb"); }
=Labat FM.book Page 138 Vendredi, 19. juin 2009 4:01 16
138
Développement XNA pour la XBox et le PC
protected override void Initialize() { engine.Update(); sound = soundBank.GetCue("Explosion1"); sound.Play(); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { engine.Update(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Lire les fichiers en streaming La méthode que nous venons de voir charge les sons en mémoire. Cette solution est peu recommandée dès que le projet dispose d’un certain nombre de pistes, surtout si elles ne sont pas compressées. La seconde méthode que nous allons détailler permet de lire les données en streaming, c’est-à-dire en chargement continu. 1. Ouvrez de nouveau XACT et le projet de la première méthode. 2. Cependant, cette fois-ci, sélectionnez Streaming dans les propriétés de la Wave Bank. Figure 7-7
Sélection du mode streaming
3. Reconstruisez le projet (F7). 4. Au niveau du code du projet, vous devez seulement appeler un autre constructeur pour la classe WaveBank. waveBank = new WaveBank(engine, @"Content\Wave Bank.xwb", 0, 2);
=Labat FM.book Page 139 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
139
Figure 7-8
Le nouveau constructeur de la classe WaveBank
5. Le premier des deux nouveaux paramètres est l’offset de démarrage de la ressource Wave Bank. Ce paramètre est utile si vous lisez un son depuis un DVD, sinon laissezle à 0. Le second paramètre est la taille du buffer utilisé pour le streaming. Vous pouvez considérer qu’il s’agit de la valeur qui déterminera la qualité du son. La valeur minimale est de 2 et il n’est pas conseillé de dépasser 16, faute de quoi le son risque de ne plus être diffusé de manière ininterrompue.
Compression Si vous utilisez le mode In Memory, vous préférerez sûrement compresser les pistes pour prendre le moins de place possible en mémoire. Retournez au projet dans XACT. Dans l’explorateur de projet, faites un clic droit sur Compression Presets, puis sur New Compression Preset. Figure 7-9
Propriété d’un nouveau preset de compression
La compression n’est pas la même pour la Xbox que pour Windows. Dans les deux cas, PCM signifie que le fichier ne sera pas compressé. Tableau 7-2 Les méthodes de compression Compression
Description
XMA (Xbox 360)
Vous devez spécifier la qualité de la piste via un curseur. Plus la valeur est faible, plus la qualité est faible, mais plus la compression est impor tante. Par défaut, la valeur est de 60.
ADPCM (Windows)
Vous devez spécifier le nombre d’échantillons par bloc. Plus le nombre est grand, plus la 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
140
Développement XNA pour la XBox et le PC
Une fois que vous avez créé un preset, vous pouvez l’appliquer soit à une piste, soit à un Wave Bank. Vous n’avez plus qu’à reconstruire le projet. Figure 7-10
Application d’un preset à une banque de sons
Ajouter un effet de réverbération L’effet de « réverb », bien connu des musiciens, vise à donner l’impression d’être dans un lieu plus ou moins vaste. Il peut être ajouté aux pistes via XACT. Dans l’explorateur de projet, ces effets sont regroupés sous le nom « DSP Effect Path Presets ». Voici comment ajouter un nouvel effet : 1
Dans XACT, cliquez avec le bouton droit sur la catégorie DSP Effect Path Presets de l’explorateur de projet, puis sélectionnez New Microsoft Reverb Project.
Figure 7-11
Le paramétrage de la réverbé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 disponibles dans la liste déroulante Effect Preset, qui permettent d’obtenir facilement la réverbération d’une cave, d’une salle de concert, d’un hangar, etc.
=Labat FM.book Page 141 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
141
3. Reste à appliquer l’effet à une piste. Cliquez avec le bouton droit sur le nom de l’effet dans l’explorateur de projet, puis cliquez sur Attach/Detach Sound(s)… À partir de la fenêtre qui s’est ouverte, attachez l’effet à chacune des pistes ou détachez-le. Figure 7-12
La fenêtre de liaison pistes/ effet
4. Pour rendre ces modifications utilisables dans le jeu, reconstruisez le projet (F7) ; aucune autre modification n’est nécessaire sous Visual Studio.
Le son avec la nouvelle API SoundEffect Vous venez de voir que XACT est très simple d’utilisation. Cependant, les développeurs de XNA ont eu des remontées de nombreuses personnes qui trouvaient son utilisation un peu lourde pour des petits projets, notamment à cause de l’utilisation d’un outil externe et de la gestion de l’arborescence de la banque de sons. Les développeurs ont donc ajouté une nouvelle API, baptisée SoundEffect, pour la gestion du son. Celle-ci rend le chargement et l’utilisation de pistes sonores aussi simples que dans le cas d’une texture, sans que vous ayez à gérer tout un projet sonore comme vous le feriez pour XACT. De plus, si vous avez l’intention de porter le projet pour Zune, sachez que seule cette nouvelle API est disponible en ce qui concerne le son.
Lire un son La lecture d’un son se fera en utilisant un objet de type SoundEffect. Ajoutez simplement un fichier .wav au projet comme vous ajouteriez une texture (voir le chapitre 6), puis créez l’objet SoundEffect et chargez le son. La lecture se fait ensuite grâce à la méthode Play(). Test de l’API SoundEffect public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SoundEffect soundEffect;
=Labat FM.book Page 142 Vendredi, 19. juin 2009 4:01 16
142
Développement XNA pour la XBox et le PC
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); soundEffect = Content.Load("Explosion1"); soundEffect.Play(); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Lire un morceau de musique Grâce à la classe MediaLibrary, vous accédez à la musique présente dans la bibliothèque multimédia de l’utilisateur. Ces musiques doivent avoir été détectées au préalable par Windows Media Player. Vous pourrez ensuite jouer un morceau d’un disque présent dans cette bibliothèque multimédia grâce à la classe MediaPlayer. Le code ci-dessous lit la première piste d’un album choisi au hasard dans la bibliothèque du joueur. Si ce dernier appuie sur la touche Espace et que la piste est en cours de lecture, elle est mise en pause. Si elle est déjà en pause, la lecture reprend. Test de classe MediaPlayer public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics;
=Labat FM.book Page 143 Vendredi, 19. juin 2009 4:01 16
La sonorisation CHAPITRE 7
SpriteBatch spriteBatch; MediaLibrary sampleMediaLibrary; Random rand; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; sampleMediaLibrary = new MediaLibrary(); rand = new Random(); } protected override void Initialize() { ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); int i = rand.Next(0, sampleMediaLibrary.Albums.Count - 1); MediaPlayer.Play(sampleMediaLibrary.Albums[i].Songs[0]); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (ServiceHelper.Get().IsKeyDown(Keys.Space)) { if (MediaPlayer.State == MediaState.Playing) MediaPlayer.Pause(); else MediaPlayer.Resume(); } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
143
=Labat FM.book Page 144 Vendredi, 19. juin 2009 4:01 16
144
Développement XNA pour la XBox et le PC
Pour un bon design sonore À première vue, le son n’est pas l’élément qui vous causera le plus de soucis lors de la création d’un jeu vidéo : il suffit juste de charger les sons et de les jouer. Cependant, les joueurs n’ont pas tous les mêmes équipements audio ! De plus, certains joueurs n’aiment pas les musiques présentes dans les jeux et préfèrent écouter les leurs… Veillez donc à leur laisser la possibilité de les éteindre sans pour autant couper tous les bruitages, par exemple par l’intermédiaire d’un menu d’options. Ci-dessous vous retrouverez quelques exemples de la liste des actions conseillées par le site du Creators Club pour le son des jeux de la Xbox 360 (la liste complète est disponible sur http://creators.xna.com/en-US/education/bestpractices). Certains de ces conseils s’appliquent aussi aux jeux développés pour les autres plates-formes. • Essayez de tester la partie sonore du jeu sur le plus grand nombre de configurations possibles (stéréo, mono, casques, etc.). • Assurez-vous, au moment de leur édition, que le volume des musiques et/ou des bruitages est normalisé afin que les joueurs n’aient pas à le modifier au cours du jeu. Pour définir cette norme, basez-vous sur le volume du son de démarrage de la Xbox 360 : réglez-le pour qu’il soit un peu fort et ajustez ensuite le volume du jeu en conséquence. • Faites attention : sur la Xbox 360, la méthode Play() de la classe MediaPlayer est asynchrone, ce qui signifie que les musiques ne sont pas immédiatement lues. Si vous vérifiez l’état de l’objet MediaPlayer dans l’appel à la méthode Update() qui suit la lecture de la musique, il sera probablement toujours sur MediaState.Stopped. Ainsi, si vous voulez démarrer une nouvelle musique à la fin de la lecture de l’ancienne, n’utilisez pas la méthode précédente, mais attendez plutôt l’événement ActiveSongChanged puis, seulement à partir de ce moment-là, vérifiez l’état du MediaPlayer. • Par défaut, le gestionnaire de contenu (le Content Processor pour être plus précis) compresse au maximum les éléments qui seront utilisés par l’API SoundEffect. Songez-y et souvenez-vous que la compression améliore la taille du jeu au détriment de la qualité du son.
En résumé Dans ce chapitre, vous avez découvert : • comment gérer un projet sonore avec XACT et utiliser ce projet dans XNA ; • quels sont les avantages et inconvénients de l’API SoundEffect par rapport à XACT et comment utiliser cette API dans XNA ; • comment utiliser la classe MediaPlayer pour jouer des musiques de la bibliothèque multimédia de l’utilisateur ; • quelques bonnes pratiques à appliquer dans le design sonore d’un jeu.
=Labat FM.book Page 145 Vendredi, 19. juin 2009 4:01 16
8 Exceptions et gestion des fichiers : sauvegarder et charger un niveau Vous allez sûrement vouloir charger des niveaux et en ajouter facilement de nouveaux aux jeux. Vous voudrez aussi probablement que l’expérience de jeu qu’auront les utilisateurs s’inscrive dans la durée en enregistrant leur progression ou leur score. Ce chapitre apporte toutes les réponses à vos questions en ce qui concerne le stockage de données sur les périphériques physiques (disque dur, carte mémoire, etc.). Il commence par une partie théorique sur les emplacements de stockage offerts par XNA, les fichiers XML et la sérialisation. Vous découvrirez ensuite les Gamer Services. Enfin, vous passerez à la pratique en apprenant à gérer et à utiliser fichiers et répertoires.
Le stockage des données Dans cette première partie, vous allez découvrir la théorie liée aux différents emplacements de stockage mis à disposition par XNA, ainsi qu’aux différentes méthodes de sauvegarde de données.
Les espaces de stockage Le framework XNA met à votre disposition deux espaces de stockage bien distincts : • Le dossier du jeu dans lequel se situe l’exécutable, ainsi que tout le contenu que vous aurez créé (textures, sons, etc.).
=Labat FM.book Page 146 Vendredi, 19. juin 2009 4:01 16
146
Développement XNA pour la XBox et le PC
Zune Sur le Zune, cet espace de stockage se limite à 16 Mo. Cela correspond à la mémoire vive qu’un jeu peut utiliser.
• Le dossier de l’utilisateur où se situent les sauvegardes ou la configuration préférée d’un joueur. Sous Windows, il s’agit du répertoire SavedGames situé dans le répertoire personnel de l’utilisateur connecté sur le PC. À l’intérieur de ce répertoire se trouve un dossier correspondant au nom du jeu exécuté. Enfin, la dernière ramification correspond au numéro du joueur (PlayerIndex). Si aucun n’est spécifié, les fichiers se trouvent dans le répertoire AllPlayers, sinon dans un répertoire correspondant à ce numéro (Player1, Player2, Player3 ou Player4). Sur Xbox, il s’agit du disque dur ou d’une carte mémoire : c’est le joueur qui choisira le périphérique de stockage qu’il souhaite utiliser.
Figure 8-1
Fichier personnel de configuration du jeu Racing Game
Lecteur réseau Si votre dossier personnel (Documents and Settings\) ne se situe pas sur l’ordinateur où vous exécuterez un jeu qui doit sauvegarder des données personnelles, mais sur un lecteur réseau, une exception sera levée rendant la sauvegarde impossible. À l’heure actuelle, l’exécution d’un jeu à travers le réseau n’est pas supportée par XNA.
Le format XML, un format intelligible qui simplifie le stockage de données XML (eXtensible Markup Language, en français langage extensible de balisage) est utilisé pour contenir des données qui seront encadrées par des balises. Si vous vous êtes déjà aventuré dans la création d’un site web, vous avez probablement rencontré le HTML qui repose également sur des balises utilisées pour formater l’affichage de données. L’autre spécificité de XML est qu’il n’y a pas une liste de balises définies : c’est à vous de créer celles qui vous seront utiles. Ainsi, XML est utilisé dans des domaines très variés : • Les fichiers de configuration de logiciels ou de jeux sont de plus en plus basés sur ce format. Certains utilisent même ces fichiers pour la configuration de l’interface utilisateur, permettant ainsi aux utilisateurs novices de la paramétrer très facilement.
=Labat FM.book Page 147 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
147
• Dans les jeux vidéos, les fichiers de sauvegarde ou ceux qui décrivent les niveaux peuvent aussi utiliser le format XML. Ci-dessous, vous retrouvez un fichier de configuration fictif, utilisant le XML, qui pourrait correspondre à la résolution de l’écran dans un de vos jeux.
Figure 8-2
Un fichier XML ouvert dans Internet Explorer 7
Cependant, beaucoup d’informaticiens ne se sont pas encore ralliés à la cause de XML. Certains préféreront utiliser des formats utilisant des séparateurs de données. L’exemple qui suit correspond au fichier de configuration fictif de l’exemple précédent, mais cette fois-ci dans le format CSV (Comma Separated Values, valeurs séparées par des virgules). 800,600
D’autres préféreront les fichiers INI, pourtant progressivement abandonnés par Microsoft depuis Windows 95. Dans ce genre de fichier, vous définissez des sections puis vous
=Labat FM.book Page 148 Vendredi, 19. juin 2009 4:01 16
148
Développement XNA pour la XBox et le PC
affectez des valeurs à différentes variables. Ci-dessous, la configuration de la résolution de l’écran au format INI. [ScreenSize] Width=800 Height=600
Libre à vous d’utiliser le format que vous préférez. Vous pouvez même en inventer un, ou utiliser des fichiers binaires (qui ne pourront pas être lus directement par un éditeur de texte). Retenez cependant que XML est un langage très simple à utiliser et très générique.
Sérialisation et désérialisation La sérialisation (en anglais serialization) est un processus qui permet d’enregistrer l’état complet d’un objet à un moment donné pour le sauvegarder ou l’envoyer vers le réseau ou un périphérique. L’état d’un objet signifie l’ensemble des valeurs de ses champs. L’opération inverse, qui consiste à reformer l’objet, s’appelle la désérialisation. Les données peuvent être sérialisées sous de nombreuses formes : fichiers binaires, XML, etc.
Les exceptions Essayez de vous connecter avec un compte qui n’est pas membre du XNA Creators Club. Que se passe-t-il ? La fenêtre du jeu se ferme et le focus est donné à Visual Studio où une bien étrange boîte de dialogue est apparue. Cela signifie qu’une exception a été levée, en l’occurrence GamerServicesNotAvailableException. Figure 8-3
Une exception a été levée
Quand une erreur survient, une exception est levée. À partir de ce moment, l’exécution normale est interrompue et un gestionnaire d’exceptions est recherché dans le bloc d’instructions courant. S’il n’est pas trouvé, la recherche se poursuit dans le bloc englo-
=Labat FM.book Page 149 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
149
bant celui-ci ou, à défaut, dans le bloc de la fonction appelante, et ainsi de suite. Si la recherche n’aboutit pas, une boîte de dialogue signalant l’exception s’affiche. Si votre jeu avait été exécuté en mode Release, par exemple par un de vos amis dont vous auriez aimé avoir l’avis, une boîte de dialogue peu commode se serait affichée (figure 8-4).
Figure 8-4
Voici la fenêtre qui apparaîtra si vous gérez mal vos exceptions
Vous allez donc devoir protéger votre jeu de ces arrêts brutaux en ajoutant autant de gestionnaires d’exceptions que nécessaire. En pratique, il faut en ajouter chaque fois qu’une portion de code est susceptible de rencontrer un problème : tentative de connexion au réseau impossible, accès à des données qui n’existent pas, division par zéro, dépassement de l’indice maximum lorsque vous manipulez des tableaux, etc. La liste peut être très longue… L’extrait de code ci-dessous vous présente la structure basique d’un gestionnaire d’exceptions. Le code susceptible de générer une exception, et qui doit dont être testé, est celui présent dans le bloc try. Vous pouvez ensuite récupérer l’exception pour la traiter dans le bloc catch. try { // Portion de code pouvant lancer une exception } catch(Exception e) { // Traitement de l'exception }
L’exemple suivant divise une variable a par une variable b. Ici vous connaissez la valeur de b, or cela n’est pas toujours le cas. Dans le doute, il est préférable d’ajouter un gestionnaire 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
150
Développement XNA pour la XBox et le PC
int a = 10; int b = 0; try { a /= b; } catch (Exception e) { Window.Title = e.Message; }
Dans l’intitulé de la fenêtre, vous pouvez lire : Tentative de division par zéro.
En C#, les exceptions particulières dérivent toutes de la classe Exception. L’exemple précédent a levé une exception de type DivideByZeroException, mais aucun bloc catch qui lui correspond n’est présent. Il en existe tout de même un traitant une exception de type Exception, c’est ce bloc qui sera exécuté. Vous pouvez donc cumuler les blocs catch de manière à effectuer un traitement spécial pour certaines erreurs. L’extrait de code ci-dessous reprend l’exemple précédent en affichant un message spécial si l’exception levée est une division par zéro et un message plus générique s’il s’agit d’une autre exception (sait-on jamais). int a = 10; int b = 0; try { a /= b; } catch (DivideByZeroException e) { Window.Title = "Je le savais"; } catch (Exception e) { Window.Title = e.Message; }
Cette fois-ci, dans l’intitulé de la fenêtre, vous pouvez lire le message personnalisé. Je le savais
Vous pouvez également ajouter un bloc finally qui sera exécuté, qu’une exception ait été levée ou non. int a = 10; int b = 0;
=Labat FM.book Page 151 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
151
try { a /= b; } catch (DivideByZeroException e) { Window.Title = "Je le savais"; } catch (Exception e) { Window.Title = e.Message; } finally { Window.Title = "Il est passé ici"; }
La levée d’une exception se fait en utilisant le mot-clé throw suivi d’un objet du type de l’exception voulue. Dans l’exemple suivant, la fonction TestException lèvera une exception si le paramètre i vaut 0. protected override void Initialize() { base.Initialize(); try { TestException(0); } catch (Exception e) { Window.Title = e.Message; } } private void TestException(int i) { if (i == 0) throw new Exception("i vaut 0 !"); } Personnaliser les exceptions Vous pouvez créer vos propres exceptions selon vos besoins. Il suffit de les faire dériver de la classe Exception.
=Labat FM.book Page 152 Vendredi, 19. juin 2009 4:01 16
152
Développement XNA pour la XBox et le PC
Les Gamer Services : interagir avec l’environnement Les Gamer Services sont un ensemble de fonctionnalités qui permettent au jeu d’interagir avec son environnement : boîte de dialogue pour informer l’utilisateur, récupérer des messages de celui-ci, afficher sa liste d’amis et surtout, accéder à un périphérique de sauvegarde.
Dossier de l’utilisateur Commencez par ajouter au jeu un nouveau composant de type GamerServicesComponant, faute de quoi vous ne pourrez pas utiliser le dossier de l’utilisateur. this.Components.Add(new GamerServicesComponent(this));
Vous êtes à présent en mesure d’utiliser toutes les fonctionnalités mises à votre disposition par la classe Guide (de l’espace de noms Microsoft.Xna.Framework.GamerServices). Par exemple, le code ci-dessous affichera l’écran de connexion au Xbox LIVE (un abonnement XNA Creators Club est requis pour pouvoir se connecter). Vous serez ensuite en mesure de récupérer des informations à propos du joueur.
Figure 8-5
L’écran de connexion au Xbox LIVE s’affiche facilement
protected override void Initialize() { base.Initialize(); Guide.ShowSignIn(1, false); }
=Labat FM.book Page 153 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
153
Grâce aux Gamer Services, vous accédez au dossier de l’utilisateur. Essayez la classe cidessous. Si vous êtes sur Xbox 360, la méthode BeginShowStorageDeviceSelector() affichera l’écran de sélection du périphérique de sauvegarde. Ensuite, la méthode EndShowStorage DeviceSelector() renverra un objet StorageDevice. La méthode OpenContainer() de cet objet renverra un objet de type StorageContainer que vous utiliserez pour vos fichiers de sauvegarde. Récupérer le dossier de l’utilisateur public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; StorageDevice device; StorageContainer container; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null); } private void GetDevice(IAsyncResult result) { if (result.IsCompleted) { try { device = Guide.EndShowStorageDeviceSelector(result); container = device.OpenContainer("ChapitreHuit"); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } }
=Labat FM.book Page 154 Vendredi, 19. juin 2009 4:01 16
154
Développement XNA pour la XBox et le PC
Attention La gestion du dossier de l’utilisateur dans un jeu sur la Xbox 360 peut s’avérer plus difficile que sous Windows. En effet, il se peut par exemple qu’un joueur débranche le périphérique sélectionné en plein jeu. Ne vous limitez donc pas sur l’utilisation de blocs try … catch.
Les méthodes asynchrones Dans les exemples précédents, vous affichiez les détails des exceptions dans la barre de titre des fenêtres. Ceci n’est pas très esthétique et reste peu pratique pour le joueur qui ne remarquera pas forcément que le titre de la fenêtre à été modifié, surtout s’il joue en plein écran… La solution est donc d’afficher le message dans la fenêtre du jeu : dans une console ou bien dans une boîte de dialogue. Vous pourrez créer facilement une boîte de dialogue si vous employez dans votre jeu un projet de GUI qui met ce genre d’éléments à votre disposition, mais vous pouvez aussi simplement utiliser les Gamer Services. La classe Guide possède une méthode BeginShowMessage (). Le tableau ci-dessous présente les différents paramètres qu’elle attend. Tableau 8-1 Paramètres de la méthode BeginShowMessage Paramètre
Description
PlayerIndex player
Joueur concerné par la boîte de dialogue. Sous Windows, il ne peut s’agir que du joueur 1.
String title
Titre de la boîte de dialogue.
String text
Contenu textuel de la boîte de dialogue.
IEnumerable buttons
Description à afficher sur chacun des boutons de la boîte de dialogue. Il peut y avoir au maximum trois boutons.
Int focusButton
Index (zéro étant la valeur minimale) du bouton qui doit avoir le focus.
MessageBoxIcon icon
Type de l’icône à afficher avec la boîte de dialogue ( Alert, Error, None, Warning).
AsyncCallback callback
La méthode à appeler une fois que l’opération asynchrone est terminée.
Object state
Un objet créé par l’utilisateur pour identifier l’appel à la méthode.
Vous vous demandez sûrement à quoi sert le paramètre callback de type AsyncCallback. Généralement, l’appel à une fonction se fait de manière synchrone, c’est-à-dire qu’aucune autre instruction n’est exécutée avant que la fonction ne retourne une valeur : 1. Appel de la fonction. 2. Exécution de la fonction. 3. Le thread qui a appelé la fonction récupère la main. Or, le traitement de certaines fonctions peut être assez long, notamment dans le cas des fonctions d’entrées-sorties ou de communication à travers le réseau. Il est donc nécessaire de les traiter en parallèle, c’est-à-dire de manière asynchrone.
=Labat FM.book Page 155 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
155
Pour effectuer un traitement asynchrone, vous aurez donc besoin de trois méthodes. La première, dont le nom commence par Begin, commande l’exécution de l’opération. Une fois celle-ci terminée, un délégué (delegate en anglais, une variable permettant d’appeler une fonction) est appelé. Celui-ci appelle alors la méthode dont le nom commence par End. Commencez par écrire la fonction qui appellera la méthode dont le nom commence par End. Elle doit prendre en paramètre une interface de type IAsyncResult, qui sera transmise à la méthode commençant par End. Cette interface dispose du booléen IsCompleted permettant de savoir si l’opération est terminée ou non. private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); }
Il ne reste plus qu’à appeler la méthode commençant par Begin dans un bloc catch. N’oubliez pas de passer en paramètre le délégué qui servira à appeler la fonction précédente. Complétez le reste des paramètres selon vos besoins. try { TestException(0); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new String[] ➥ { "OK" }, 0, MessageBoxIcon.Error, new AsyncCallback(EndShowMessageBox), null); }
Vous trouverez ci-dessous le code source complet de la classe illustrant la notion qui vient d’être présentée. Utiliser des méthodes asynchrones public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); try { TestException(0);
=Labat FM.book Page 156 Vendredi, 19. juin 2009 4:01 16
156
Développement XNA pour la XBox et le PC
} catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ String[] { "OK" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } private void TestException(int i) { if (i == 0) throw new Exception("i vaut 0 !"); } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
Figure 8-6
La boîte de dialogue affiche le détail de l’exception
Grâce aux Gamer Services, vous accédez également à une boîte de dialogue permettant à l’utilisateur d’entrer une chaîne de caractères : il suffit d’utiliser les fonctions BeginShow KeyboardInput () et EndShowKeyboardInput(). Le tableau ci-dessous répertorie tous les paramètres attendus par la première fonction.
=Labat FM.book Page 157 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
157
Tableau 8-2 Paramètres de la fonction BeginShowKeyboardInput Paramètre
Description
PlayerIndex player
Joueur concerné par la boîte de dialogue. Sous Windows, il ne peut s’agit que du joueur un.
String title
Titre de la boîte de dialogue.
String text
Contenu textuel de la boîte de dialogue.
String defaultText
Texte à afficher dans la boîte de dialogue lorsque celle-ci s’ouvre.
AsyncCallback callback
La méthode à appeler une fois que l’opération asynchrone est terminée.
Object state
Un objet créé par l’utilisateur pour identifier l’appel à la méthode.
Ci-dessous, vous retrouvez le code source d’une classe exemple utilisant ce type de boîte de dialogue. Utiliser la fenêtre KeyboardInput public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowKeyboardInput(PlayerIndex.One, "Entrez un message", "Entrez ➥ un message pour cet exemple", "", new AsyncCallback(EndShowKeyboardInput), ➥ null); } private void EndShowKeyboardInput(IAsyncResult result) { string userInput = Guide.EndShowKeyboardInput(result); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
=Labat FM.book Page 158 Vendredi, 19. juin 2009 4:01 16
158
Développement XNA pour la XBox et le PC
Figure 8-7
Le joueur peut aisément écrire un message dans cette boîte de dialogue
La GamerCard : la carte d’identité du joueur Vous pouvez afficher les informations sur un joueur connecté grâce à la méthode ShowGamerCard () de la classe Guide. Elle attend comme paramètre un PlayerIndex, ainsi qu’un objet de type Gamer. Vous pouvez récupérer la liste des joueurs connectés grâce à la collection SignedInGamers de la classe SignedInGamer. La classe ci-dessous invite le joueur à se connecter puis, lorsque celui-ci pressera la touche A de son clavier, elle affichera des informations le concernant. N’oubliez pas d’ajouter un bloc try … catch, notamment au cas où aucun joueur ne serait connecté. Le test sur la propriété IsVisible permet de ne pas demander un nouvel affichage du Guide si celui-ci est déjà présent à l’écran. Afficher la GamerCard du joueur connecté public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.ShowSignIn(1, true); }
=Labat FM.book Page 159 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
protected override void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.A) && !Guide.IsVisible) { try { Guide.ShowGamerCard(PlayerIndex.One, SignedInGamer ➥ .SignedInGamers[0]); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } base.Update(gameTime); } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
Figure 8-8
À partir de ce panneau, le joueur peut administrer son compte
159
=Labat FM.book Page 160 Vendredi, 19. juin 2009 4:01 16
160
Développement XNA pour la XBox et le PC
De la même manière, vous pouvez permettre au joueur d’écrire un message avec Show ComposeMessage(), d’afficher la liste de ses amis avec ShowFriends(), d’afficher la fenêtre d’ajout d’un ami avec ShowFriendRequest(), etc. La liste est encore longue, à vous de l’explorer et d’utiliser ces méthodes selon vos besoins.
Version démo Si vous avez décidé de créer un jeu que vous vendrez ensuite à la communauté, vous pouvez lui ajouter un mode de démonstration (trial mode). Bien évidemment, dans ce mode, vous limiterez la liberté du joueur vis-à-vis des possibilités offertes par le jeu. Pour que vous, développeur, puissiez tester ce mode démonstration, vous devez définir le booléen SimulateTrialMode à true. Vous pouvez ensuite savoir si le jeu est exécuté en mode démonstration via le booléen IsTrialMode. Essayez la classe suivante. Vérifier s’il s’agit d’une version d’essai public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Update(GameTime gameTime) { if (Guide.IsTrialMode) Window.Title = "Trial Mode"; else Window.Title = "Full Mode"; base.Update(gameTime); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
=Labat FM.book Page 161 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
161
Dans la barre de titre de la fenêtre, vous pouvez lire : Full Mode
Maintenant, modifiez la classe de manière à ce que la propriété SimulateTrialMode soit définie à true. Simuler une version d’essai public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.SimulateTrialMode = true; } protected override void Update(GameTime gameTime) { if (Guide.IsTrialMode) Window.Title = "Trial Mode"; else Window.Title = "Full Mode"; base.Update(gameTime); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
Vous pouvez à présent lire : Trial Mode
=Labat FM.book Page 162 Vendredi, 19. juin 2009 4:01 16
162
Développement XNA pour la XBox et le PC
La sauvegarde en pratique : réalisation d’un éditeur de cartes Pour vous aider à bien comprendre comment mettre en pratique une solution de sauvegarde et de chargement de données dans le jeu, nous allons maintenant développer un petit éditeur de cartes en 2D.
Identifier les besoins Commençons par définir les besoins auxquels devra répondre l’éditeur de cartes. Lorsque l’utilisateur appuiera sur la touche C de son clavier, une carte vierge sera générée. L’utilisateur déplacera ensuite un curseur sur la carte grâce aux touches fléchées du clavier. Il pourra ensuite choisir la texture à utiliser sur chacune des cases de la carte grâce aux touches fonctions (F1, F2, etc.). Lorsque l’utilisateur pressera la touche S du clavier, la carte sera sauvegardée. Il pourra ensuite fermer le programme, le rouvrir et recharger sa carte en appuyant sur la touche L. La première chose à faire est de créer un projet partagé de type Windows Game Library. Dans ce projet, créez la classe correspondant aux cases de la carte. La classe s’appellera Tile. Elle contiendra une texture, une chaîne de caractères correspondant au nom de fichier de la texture et enfin, une paire de coordonnées correspondant à sa position logique sur la carte. public class Tile { Texture2D texture; string assetName; public string AssetName { get { return assetName; } set { assetName = value; } } Vector2 position; public Vector2 Position { get { return position; } set { position = value; } }
=Labat FM.book Page 163 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
163
public Tile() { } public Tile(string assetName, Vector2 position) { this.assetName = assetName; this.position = position; } public void LoadContent(ContentManager Content) { texture = Content.Load(assetName); } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, new Vector2(position.X * texture.Width, position.Y ➥ * texture.Height), Color.White); } }
Occupons-nous maintenant de la classe qui correspond à la carte. Cette classe contiendra simplement un tableau de tableau de Tile.public class Map. { Tile[][] tiles; public Tile[][] Tiles { get { return tiles; } set { tiles = value; } } public Map() { } public Map(Vector2 size) { tiles = new Tile[(int)size.Y][]; for(int i = 0; i < tiles.Length; i ++) tiles[i] = new Tile[(int)size.X]; }
=Labat FM.book Page 164 Vendredi, 19. juin 2009 4:01 16
164
Développement XNA pour la XBox et le PC
public void LoadContent(ContentManager Content) { for (int y = 0; y < tiles.Length; y++) { for (int x = 0; x < tiles[0].Length; x++) { tiles[y][x].LoadContent(Content); } } } public void Draw(SpriteBatch spriteBatch) { for (int y = 0; y < tiles.Length; y++) { for (int x = 0; x < tiles[0].Length; x++) { tiles[y][x].LoadContent(Content); } } } }
La dernière classe à préparer est celle du curseur. Il s’agit d’un simple sprite qui devra se déplacer selon les entrées clavier de l’utilisateur. public class Cursor { Texture2D texture; Vector2 position; public Vector2 Position { get { return position; } set { position = value; } } KeyboardState keyboardState; KeyboardState lastKeyboardState; public Cursor() { position = new Vector2(0, 0); }
=Labat FM.book Page 165 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
165
public void LoadContent(ContentManager Content) { texture = Content.Load("cursor"); } public void Update(GameTime gameTime, Vector2 mapSize) { lastKeyboardState = keyboardState; keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left) && lastKeyboardState ➥ .IsKeyUp(Keys.Left) && position.X > 0) { position.X--; } if (keyboardState.IsKeyDown(Keys.Right) && lastKeyboardState ➥ .IsKeyUp(Keys.Right) && position.X < mapSize.X-1) { position.X++; } if (keyboardState.IsKeyDown(Keys.Up) && lastKeyboardState.IsKeyUp(Keys.Up) ➥ && position.Y > 0) { position.Y--; } if (keyboardState.IsKeyDown(Keys.Down) && lastKeyboardState ➥ .IsKeyUp(Keys.Down) && position.Y < mapSize.Y-1) { position.Y++; } } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, new Vector2(position.X * texture.Width, position.Y ➥ * texture.Height), Color.White); } }
Pour terminer, référencez le projet de type Windows Game Library dans le projet principal. Pour ce faire, dans l’explorateur de solution faites un clic droit sur le conteneur de Références du projet principal, puis choisissez Ajouter une référence. Allez ensuite sur l’onglet Projet et choisissez le projet partagé. Nous avons maintenant tous les outils en main pour mettre en place l’éditeur de cartes. Ajoutons des objets de type Map et Cursor à la classe principale du projet. Si l’utilisateur appuie sur la touche C du clavier, nous initialisons la carte et le curseur. Dans le cas de la carte, on charge la même texture pour toutes les cases de la carte.
=Labat FM.book Page 166 Vendredi, 19. juin 2009 4:01 16
166
Développement XNA pour la XBox et le PC
Ensuite, si la carte et le curseur existent bien, nous vérifierons si une touche Fx est pressée et modifions la texture de la case concernée en conséquence. public class Chapitre_8 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; KeyboardState keyboardState; KeyboardState lastKeyboardState; Map map; Cursor cursor; Vector2 mapSize = new Vector2(5, 5); public Chapitre_8() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; graphics.PreferredBackBufferHeight = 160; graphics.PreferredBackBufferWidth = 160; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { lastKeyboardState = keyboardState; keyboardState = Keyboard.GetState(); if(keyboardState.IsKeyDown(Keys.C) && lastKeyboardState.IsKeyUp(Keys.C)) { map = new Map(); map = new Map(mapSize);
=Labat FM.book Page 167 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
for (int y = 0; y < map.Tiles.Length; y++) { for (int x = 0; x < map.Tiles[0].Length; x++) { map.Tiles[y][x] = new Tile("grass", new Vector2(x, y)); } } map.LoadContent(Content); cursor = new Cursor(); cursor.LoadContent(Content); } else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState ➥ .IsKeyUp(Keys.S) && map != null) { } else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState ➥ .IsKeyUp(Keys.L)) { } if (cursor != null && map != null) { cursor.Update(gameTime, mapSize); if (keyboardState.IsKeyDown(Keys.F1) && lastKeyboardState ➥ .IsKeyUp(Keys.F1)) { map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName ➥ = "grass"; map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X] ➥ .LoadContent(Content); } else if (keyboardState.IsKeyDown(Keys.F2) && lastKeyboardState ➥ .IsKeyUp(Keys.F2)) { map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName ➥ = "tree"; map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X] ➥ .LoadContent(Content); } else if (keyboardState.IsKeyDown(Keys.F3) && lastKeyboardState ➥ .IsKeyUp(Keys.F3)) { map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X].AssetName ➥ = "sand"; map.Tiles[(int)cursor.Position.Y][(int)cursor.Position.X] ➥ .LoadContent(Content); } } }
167
=Labat FM.book Page 168 Vendredi, 19. juin 2009 4:01 16
168
Développement XNA pour la XBox et le PC
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); if (map != null) map.Draw(spriteBatch); if (cursor != null) cursor.Draw(spriteBatch); spriteBatch.End(); }
Vous pouvez à présent exécuter l’éditeur de cartes et vous amuser un peu avec (figure 8-9). Figure 8-9
L’éditeur de cartes
Chemin du dossier de jeu Vous pouvez récupérer le chemin du dossier du jeu grâce à la propriété statique Title Location de l’objet StorageContainer. L’exemple suivant affiche ce chemin à la place du titre du jeu. Récupérer le chemin du dossier de jeu public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { Window.Title = StorageContainer.TitleLocation; base.Initialize(); }
=Labat FM.book Page 169 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
169
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Figure 8-10
Le chemin complet vers le dossier du jeu
Gérer les dossiers C’est l’espace de noms System.IO qui contient les classes permettant de gérer fichiers et répertoires. Le tableau ci-dessous présente quelques-unes des méthodes statiques mises à disposition par la classe Directory. Il en existe beaucoup d’autres, mais seules celles-ci seront utilisées dans le cas présent. Tableau 8-3 Méthodes de la classe Directory Méthode
Description
Bool Exists(string path)
Renvoie vrai si le répertoire correspondant au chemin passé en argument existe.
DirectoryInfo CreateDirectory(string path)
Crée tous les répertoires et sous-répertoires correspondant au chemin passé en argument.
Void Delete(string path)
Supprime le répertoire correspondant au chemin passé en argument.
=Labat FM.book Page 170 Vendredi, 19. juin 2009 4:01 16
170
Développement XNA pour la XBox et le PC
L’exemple suivant utilise ces trois méthodes. Si le joueur presse la touche C et que le répertoire test n’existe pas dans le dossier de l’utilisateur (on récupère le chemin via la propriété Path de l’objet de type StorageContainer), on le crée. Si le joueur presse la touche D et que le répertoire existe, on le supprime. Gérer les dossiers dans le répertoire de l’utilisateur public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; StorageDevice device; StorageContainer container; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null); } protected override void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.C)) { try { if(!Directory.Exists(Path.Combine(container.Path, "test"))) Directory.CreateDirectory(Path.Combine(container.Path, "test")); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } else if (Keyboard.GetState().IsKeyDown(Keys.D)) { try {
=Labat FM.book Page 171 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
171
if (Directory.Exists(Path.Combine(container.Path, "test"))) Directory.Delete(Path.Combine(container.Path, "test")); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } private void GetDevice(IAsyncResult result) { if (result.IsCompleted) { try { device = Guide.EndShowStorageDeviceSelector(result); container = device.OpenContainer("ChapitreHuit"); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } }
Dans cet exemple, vous remarquez que la méthode Combine de la classe Path est utilisée pour créer le chemin du répertoire. Vous n’avez donc pas à vous soucier des séparateurs / ou \.
=Labat FM.book Page 172 Vendredi, 19. juin 2009 4:01 16
172
Développement XNA pour la XBox et le PC
La classe Directory possède d’autres méthodes qui pourront vous servir dans le développement des jeux. Ainsi, la méthode GetFiles() retournera un tableau de chaînes de caractères correspondant à la liste des fichiers présents dans un répertoire. L’une de ses surcharges vous permet également d’ajouter un filtre sur les noms de fichiers à récupérer.
Manipuler les fichiers La manipulation de fichiers se fera grâce à la classe File. Elle possède un très grand nombre de méthodes, mais nous ne les présenterons pas toutes ici. Pour commencer, cette classe dispose, comme la classe Directory, d’une méthode Exists() fonctionnant exactement de la même manière. Pour créer un fichier, vous pouvez utiliser la méthode Create (). Elle attend comme seul paramètre le chemin du fichier à créer. Cependant, elle dispose de surcharges vous permettant de définir la taille du buffer qui sera utilisé pour la lecture et l’écriture, la méthode de création du fichier ou encore les options de sécurité à appliquer au fichier. La méthode retourne un objet de type FileStream. Si vous ne souhaitez pas modifier tout de suite le contenu du fichier, fermez-le directement avec la fonction Close(). Bonne pratique Une bonne habitude à prendre en programmation est de toujours fermer les fichiers que vous avez créés ou ouverts lorsque vous n’en avez plus besoin.
Vous pouvez copier un fichier grâce à la méthode Copy (). Elle prend en paramètre le chemin du fichier à copier et le chemin du fichier de destination. Elle possède une surcharge qui vous propose de définir si le fichier de destination doit être écrasé lorsqu’il existe déjà. Pour supprimer un fichier, utilisez simplement la méthode Delete () qui attend comme argument le chemin du fichier à supprimer. La classe ci-dessous utilise toutes ces méthodes. Si le joueur appuie sur la touche C et que le fichier test.sav n’existe pas dans le dossier de l’utilisateur, il est créé ; s’il existe, il est copié en test_copy.sav en écrasant le fichier de destination. Si le joueur appuie sur D et que test.sav existe, il est supprimé. Gérer les fichiers dans le répertoire de l’utilisateur public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; StorageDevice device; StorageContainer container;
=Labat FM.book Page 173 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null); } protected override void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.C)) { try { if(!File.Exists(Path.Combine(container.Path, "test.sav"))) File.Create(Path.Combine(container.Path, "test.sav")).Close(); if(File.Exists(Path.Combine(container.Path, "test.sav"))) File.Copy(Path.Combine(container.Path, "test.sav") ➥ ,Path.Combine(container.Path, "test_copy.sav"), true); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } else if (Keyboard.GetState().IsKeyDown(Keys.D)) { try { if (File.Exists(Path.Combine(container.Path, "test.sav"))) File.Delete(Path.Combine(container.Path, "test.sav")); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } base.Update(gameTime); }
173
=Labat FM.book Page 174 Vendredi, 19. juin 2009 4:01 16
174
Développement XNA pour la XBox et le PC
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } private void GetDevice(IAsyncResult result) { if (result.IsCompleted) { try { device = Guide.EndShowStorageDeviceSelector(result); container = device.OpenContainer("ChapitreHuit"); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } }
Écrire dans un fichier Pour écrire dans un fichier, commencez par récupérer un flux en écriture vers le fichier désiré. Cette opération se fera grâce à la méthode Open () de la classe File. Le tableau 8-4 détaille les paramètres attendus par la méthode. Tableau 8-4 Paramètres de la méthode Open Paramètre
Description
String path
Chemin du fichier à ouvrir.
FileMode mode
Méthode d’ouverture à utiliser sur le fichier. FileMode est une énumération qui peut prendre plusieurs valeurs (ajout à la fin du fichier, vider le fichier, ouverture classique, etc.).
FileAccess access
Définit les opérations qui pourront être effectuées sur le fichier (lecture, écriture ou les deux).
FileShare share
Définit le type d’accès que les autres threads ont sur le fichier.
=Labat FM.book Page 175 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
175
Utilisez ensuite un objet de type StreamWriter pour écrire dans le flux récupéré précédemment. Comme pour le flux de sortie standard (souvenez-vous de vos premiers programmes en mode console), l’écriture se fait grâce aux méthodes Write () et WriteLine (). N’oubliez pas ensuite de vider le buffer grâce à la méthode Flush () et enfin de fermer le flux par la méthode Close (). La classe suivante met ces actions en pratique. Lorsque le joueur appuie sur la touche O, on vérifie si le fichier test.sav existe dans le répertoire de l’utilisateur. Si c’est le cas, on se place à la fin du fichier, sinon il est créé. Il ne reste plus qu’à ajouter une ligne au flux, puis à fermer le fichier proprement. Après avoir testé le code, vous lirez dans le fichier la phrase suivante : XNA’s Not Acronymed
Ouvrir un fichier et écrire dans un fichier public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; StorageDevice device; StorageContainer container; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null); } protected override void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.O)) { try { FileStream file = File.Open(Path.Combine(container.Path, ➥ "test.sav"), FileMode.Append); StreamWriter fileWriter = new StreamWriter(file); fileWriter.WriteLine("XNA's Not Acronymed"); fileWriter.Flush(); fileWriter.Close(); }
=Labat FM.book Page 176 Vendredi, 19. juin 2009 4:01 16
176
Développement XNA pour la XBox et le PC
catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, new ➥ string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } private void GetDevice(IAsyncResult result) { if (result.IsCompleted) { try { device = Guide.EndShowStorageDeviceSelector(result); container = device.OpenContainer("ChapitreHuit"); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } }
Lire un fichier C’est une bonne chose de savoir écrire, c’est encore mieux de pouvoir lire ce qui a été écrit. Ainsi, pour lire des données écrites en clair, commencez par récupérer un flux FileStream que vous exploiterez grâce aux méthodes Read(), ReadLine(), ReadToEnd(), etc., d’un objet StreamReader.
=Labat FM.book Page 177 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
Lire le contenu d’un fichier public class ChapitreHuit : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; StorageDevice device; StorageContainer container; public ChapitreHuit() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; this.Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); Guide.BeginShowStorageDeviceSelector(new AsyncCallback(GetDevice), null); } protected override void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.L) && File.Exists ➥ (Path.Combine(container.Path, "test.sav"))) { try { FileStream file = File.Open(Path.Combine(container.Path, ➥ "test.sav"), FileMode.Open); StreamReader fileReader = new StreamReader(file); Window.Title = fileReader.r(); fileReader.Close(); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); }
177
=Labat FM.book Page 178 Vendredi, 19. juin 2009 4:01 16
178
Développement XNA pour la XBox et le PC
private void GetDevice(IAsyncResult result) { if (result.IsCompleted) { try { device = Guide.EndShowStorageDeviceSelector(result); container = device.OpenContainer("ChapitreHuit"); } catch (Exception e) { Guide.BeginShowMessageBox(PlayerIndex.One, "Erreur", e.Message, ➥ new string[] { "Ok" }, 0, MessageBoxIcon.Error, new ➥ AsyncCallback(EndShowMessageBox), null); } } } private void EndShowMessageBox(IAsyncResult result) { if (result.IsCompleted) Guide.EndShowMessageBox(result); } }
Sérialiser des données Reprenons maintenant l’éditeur de cartes que nous avons commencé à réaliser plus tôt dans ce chapitre. Nous allons à présent voir comment sérialiser les données : dans un premier temps en binaire, puis en XML. Pour qu’une classe puisse être sérialisée, il faut lui ajouter l’attribut [Serializable]. Ajoutons-le aux classes Map et Tile. [Serializable] public class Map { … } [Serializable] public class Tile { … }
Nous ne devons pas sérialiser la texture de chaque Tile, mais seulement son nom. Pour qu’un attribut ne soit pas sérialisé, il faut le marquer comme [NonSerialized]. [NonSerialized] Texture2D texture;
Pour sérialiser les données en binaire, utilisez l’espace de noms System.Runtime.Serialization .Formatters.Binary. Ensuite, si le joueur presse la touche S, vérifiez l’existence du fichier de destination et créez-le si nécessaire, sinon ouvrez-le (dans l’exemple, il est tronqué à l’ouverture). Utilisez ensuite un objet de type BinaryFormatter et sa méthode Serialize() à laquelle vous devrez passer le flux où écrire et l’objet à sérialiser. Pour terminer, fermez le flux.
=Labat FM.book Page 179 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
179
else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState.IsKeyUp(Keys.S) && map ➥ != null) { FileStream file; if (!File.Exists(Path.Combine(container.Path, "test.sav"))) file = File.Create(Path.Combine(container.Path, "test.sav")); else file = File.Open(Path.Combine(container.Path, "test.sav"), ➥ FileMode.Truncate); BinaryFormatter serializer = new BinaryFormatter(); serializer.Serialize(file, map); file.Close(); }
Si vous vous rendez dans le répertoire de sauvegarde du jeu, vous verrez le fichier test.sav. Vous pourrez ensuite l’ouvrir et constater que les données ne sont pas du tout intelligibles (figure-8-11).
Figure 8-11
Les deux fichiers créés par la classe exemple
Vous pouvez également sérialiser les données sous forme de fichier XML. L’espace de noms à utiliser est System.XML.Serialization. Une classe peut être sérialisée en XML uniquement si elle possède un constructeur sans paramètres. Il faut également que tous les attributs à sérialiser possèdent un accesseur en lecture et un autre en écriture. S’il y a des paramètres que la sérialisation doit ignorer, utilisez l’attribut [XmlIgnore]. Du côté de la classe Chapitre_8, instanciez un objet XmlSerializer auquel vous devrez passer le type d’objet à sérialiser. Servez-vous pour cela de l’opérateur typeof(). Enfin, comme pour le BinaryFormatter, utilisez la méthode Serialize() qui attend les mêmes arguments. N’oubliez pas de fermer le flux après utilisation.
=Labat FM.book Page 180 Vendredi, 19. juin 2009 4:01 16
180
Développement XNA pour la XBox et le PC
else if (keyboardState.IsKeyDown(Keys.S) && lastKeyboardState.IsKeyUp(Keys.S) && map ➥ != null) { FileStream file; if (!File.Exists(Path.Combine(container.Path, "test.xml"))) file = File.Create(Path.Combine(container.Path, "test.xml")); else file = File.Open(Path.Combine(container.Path, "test.xml"), ➥ FileMode.Truncate); XmlSerializer serializer = new XmlSerializer(typeof(Map)); serializer.Serialize(file, map); file.Close(); }
Vous pouvez tester l’éditeur en créant une carte et en la sauvegardant : un fichier XML est bel et bien généré (figure-8-12). Figure 8-12
Le fichier XML représentant la carte
Désérialiser des données Vous savez maintenant comment sauvegarder des données, il ne vous reste plus qu’à apprendre à les charger ! Ce processus se fera très simplement en utilisant la méthode Deserialize () de l’objet BinaryFormatter. Il faut ensuite effectuer un cast (c’est-à-dire une modification de type) de l’objet retourné par la fonction pour récupérer un objet Map.
=Labat FM.book Page 181 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
181
Après avoir récupéré l’objet Map, n’oubliez pas d’appeler la méthode LoadContent() pour charger les textures. else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState.IsKeyUp(Keys.L)) { FileStream file = File.Open(Path.Combine(container.Path, "test.sav"), ➥ FileMode.Open); BinaryFormatter serializer = new BinaryFormatter(); map = (Map)serializer.Deserialize(file); file.Close(); map.LoadContent(Content); cursor = new Cursor(); cursor.LoadContent(Content); }
Voyons à présent la désérialisation XML. Dans la classe Chapitre_8, vous utiliserez la méthode Deserialize() à laquelle vous passerez le flux à traiter. Transformez ensuite l’objet retourné par la méthode en un objet de type Map avec la méthode du casting. Attention, lorsque vous utilisez la sérialisation XML : tout n’est pas sérialisable. C’est pour cette raison que nous avons utilisé un tableau de tableau de Tile plutôt qu’un tableau à deux dimensions. else if (keyboardState.IsKeyDown(Keys.L) && lastKeyboardState.IsKeyUp(Keys.L)) { FileStream file = File.Open(Path.Combine(container.Path, "test.xml"), ➥ FileMode.OpenOrCreate); XmlSerializer deserializer = new XmlSerializer(typeof(Map)); map = (Map)deserializer.Deserialize(file); file.Close(); map.LoadContent(Content); cursor = new Cursor(); cursor.LoadContent(Content); }
L’éditeur de cartes est maintenant prêt : vous pouvez créer, sauvegarder et charger des cartes en utilisant soit la sérialisation binaire, soit la sérialisation XML.
Les Content Importers, une solution compatible avec la Xbox 360 Si vous essayez de désérialiser une carte comme vous venez de le voir dans un projet exécuté sur Xbox 360, vous vous rendrez compte que cela ne fonctionne pas. Sur cette plate-forme, vous ne pouvez charger que des fichiers .xnb et, pour générer de tels fichiers à partir des types personnalisés (comme la classe Map), vous devrez créer un ContentImporter.
=Labat FM.book Page 182 Vendredi, 19. juin 2009 4:01 16
182
Développement XNA pour la XBox et le PC
1. Ajoutez un projet de type ContentPipelineExtension à la solution. 2. Ajoutez à ce nouveau projet une référence vers le projet partagé qui contient les classes Map, Tile et Cursor. 3. Ajoutez ensuite au projet un nouvel élément de type Content Type Writer que vous nommerez TileWriter. 4. Renseignez le type de données concernées par le Content Type Writer. using TWrite = Chapitre_8_Shared.Tile;
5. Il faut ensuite compléter la méthode Write(). Le principe est simple : vous disposez de l’objet (value) et vous écrivez le contenu de ses différents paramètres sur l’objet output. 6. Écrivez le nom de sa texture en utilisant la méthode Write() de l’objet output. De la même manière, occupez-vous de l’attribut position. protected override void Write(ContentWriter output, TWrite value) { output.Write(value.AssetName); output.Write(value.Position); }
À présent, les données de type Tile peuvent être sérialisées vers un fichier .xnb. Intéressons-nous maintenant à leur lecture. 1. Dans le projet de bibliothèque de classes, ajoutez un nouvel élément de type Content Type Reader. 2. Comme pour le Writer, commencez par renseigner le type de données concernées. Using Tread = Chapitre_8_Shared.Tile;
3.
À présent, c’est la méthode Read() que vous allez devoir compléter. Commencez par créer une nouvelle instance de Tile. Ensuite, récupérez la valeur des deux attributs grâce aux méthodes ReadString() et ReadVector2(). Enfin, n’oubliez pas de retourner l’objet recréé. protected override TRead Read(ContentReader input, TRead existingInstance) { existingInstance = new TRead(); existingInstance.AssetName = input.ReadString(); existingInstance.Position = input.ReadVector2(); return existingInstance; }
4. Il reste une dernière chose à faire pour la classe Tile. Retournez sur le Writer et complétez la méthode GetRuntimeReader().
=Labat FM.book Page 183 Vendredi, 19. juin 2009 4:01 16
Exceptions et gestion des fichiers : sauvegarder et charger un niveau CHAPITRE 8
183
public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(Chapitre_8_Shared.TileReader).AssemblyQualifiedName; }
C’est fini pour la classe Tile. 5. Répétez ces opérations pour la classe Map. Il y a cependant un petite différence : vous ne pouvez pas utiliser la méthode Write() puisque vous avez défini le type de l’attribut Tiles (un tableau de tableau de Tile). Vous devrez utiliser la méthode générique WriteObject(). protected override void Write(ContentWriter output, TWrite value) { output.WriteObject(value.Tiles); }
6. Le raisonnement est le même que pour le Reader : vous devez utiliser la méthode ReadObject(). protected override TRead Read(ContentReader input, TRead existingInstance) { existingInstance = new Map(); existingInstance.Tiles = input.ReadObject(); return existingInstance; }
7. N’oubliez pas la méthode GetRuntimeReader(). public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(Chapitre_8_Shared.MapReader).AssemblyQualifiedName; }
8. Le ContentImporter est fin prêt. Cependant, vous devez lui fournir un fichier XML en entrée. Pour créer ce fichier, commencez par ajouter une nouvelle référence au projet principal vers l’assembly Microsoft.XNA.Framework.Content.Pipeline. Ajoutez ensuite une directive using à la classe Chapitre_8. using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;
9. Pour générer un fichier XML correspondant à la carte, vous utiliserez un objet de type XmlWritter qui correspondra au flux de sortie. Vous sérialisez ensuite les données grâce à la méthode statique Serialize de la classe IntermediateSerializer(). XmlWriterSettings xmlSettings = new XmlWriterSettings(); xmlSettings.Indent = true; using (XmlWriter xmlWritter = XmlWriter.Create("map.xml", xmlSettings)) { IntermediateSerializer.Serialize(xmlWritter, map, null); }
=Labat FM.book Page 184 Vendredi, 19. juin 2009 4:01 16
184
Développement XNA pour la XBox et le PC
Vous pouvez exécuter l’éditeur, créer une carte et la sauvegarder. 10. Rendez-vous dans le répertoire de sortie du projet et vérifiez l’existence du fichier map.xml. 11. À présent, ajoutez une référence vers le projet d’extension du Content Pipeline au projet de contenu. Ajoutez ensuite le fichier map.xml au projet de contenu comme vous ajouteriez n’importe quel autre type de ressource. À ce stade, si vous compilez le projet, vous remarquez que la génération d’un fichier .xnb correspondant à la carte. Il ne vous reste plus qu’à le charger comme vous le feriez avec n’importe quel type de ressource de base de XNA. map = Content.Load("map");
En résumé Dans ce chapitre vous avez découvert : • les différents types d’espace de stockage disponibles avec XNA ; • ce qu’est un fichier XML et comment en créer un ; • comment manipuler dossiers et fichiers en C# ; • ce qu’est la sérialisation, qu’elle soit binaire ou XML, et comment l’utiliser dans le cadre d’un projet sous XNA.
=Labat FM.book Page 185 Vendredi, 19. juin 2009 4:01 16
9 Pathfinding : programmer les déplacements des personnages Comment le héros d’un jeu vidéo fait-il pour trouver rapidement la sortie d’un labyrinthe alors que le joueur a simplement cliqué sur la sortie de celui-ci ? Après une introduction sur l’intelligence artificielle et les algorithmes de recherche de chemin, vous découvrirez en détail l’algorithme A*, puis vous apprendrez à le mettre en œuvre. À la fin de chapitre vous serez donc capable de répondre à cette question, et même mieux, vous apprendrez à programmer le comportement du héros qui doit sortir du labyrinthe.
Les enjeux de l’intelligence artificielle Pour rendre un jeu intéressant pour le joueur, vous allez par exemple devoir y introduire des mécanismes liés à l’intelligence artificielle. Le terme intelligence artificielle est difficilement définissable, puisque même les experts du domaine ne s’accordent pas sur sa signification. Certains diront qu’une machine est intelligente dès lors qu’elle sait reproduire le comportement d’un être humain, par exemple en accomplissant des tâches que l’homme sait lui aussi accomplir grâce à son intelligence. À cela, d’autres répondront qu’on ne peut pas parler ici d’intelligence, mais de simple copie du comportement. Il y a de très nombreuses choses à dire sur ce débat, puisque celui-ci relève même pour certaines personnes du domaine de l’éthique. Mais là n’est pas l’objectif de ce chapitre !
=Labat FM.book Page 186 Vendredi, 19. juin 2009 4:01 16
186
Développement XNA pour la XBox et le PC
La recherche sur l’intelligence artificielle évolue rapidement. De nos jours, un champion du monde d’échecs se fait battre par un ordinateur. Peut-être que dans un avenir proche, les choses vont prendre une dimension encore plus grande. Des scientifiques travaillent sur un système capable de représenter des pensées en images, d’autres ont créé un robot qui fonctionne avec un cerveau contrôlé par des neurones de rat. Le futur présenté dans les films de science-fiction de ces 30 dernières années semble arriver bien plus vite que n’importe qui aurait pu l’imaginer. XNA ne propose pas de classes ou de fonctions prêtes à l’emploi en rapport avec n’importe quel domaine de l’intelligence artificielle. C’est à vous, développeur, de programmer pour les jeux les algorithmes qui vous intéressent.
Comprendre le pathfinding En programmation de jeu vidéo, on appelle pathfinding (recherche de chemin, en français) le processus de détermination du chemin entre un point de départ et un point d’arrivée. Le tableau 9-1 présente quelques algorithmes de recherche de chemin. Tableau 9-1 Algorithmes de recherche de chemin Nom
Description
Dijkstra
Il retourne le meilleur chemin. Il est utilisé par exemple dans certains protocoles de routage réseau.
Viterbi
Il permet de corriger les erreurs survenues lors d’une transmission via un canal bruité.
A* (prononcez « A Star »)
Il ne retourne pas forcément la meilleure solution, mais c’est un bon compromis entre pertinence du résultat et coût du calcul.
Les domaines d’application du pathfinding sont nombreux et variés : GPS, robotique, réseaux informatiques, jeux vidéo etc. Dans tous ces domaines, le processus de détermination du chemin à emprunter est essentiel. Ainsi, si on considère les jeux de stratégie, des milliers de personnages peuvent se déplacer en même temps : l’ordinateur ou la console effectue sans relâche des calculs pour permettre aux différentes entités de se déplacer. Il faut donc trouver un moyen rapide d’effectuer ces calculs tout en conservant des résultats pertinents. Soyez cependant vigilant : si la solution que vous mettez en place ne retourne pas des résultats assez rapidement, le jeu sera saccadé. En vous reportant au tableau précédent, vous trouverez facilement quel algorithme est adapté aux jeux vidéo. Effectivement, il s’agit de A*. Dans la suite de ce chapitre, nous nous intéresserons à son principe de fonctionnement, puis à sa mise en œuvre en C# avec XNA. Attention, il a été question de performances quelques lignes plus tôt : l’algorithme tel qu’il est présenté ici est loin d’être vraiment utilisable dans une grosse production. Il y a beaucoup de choses à améliorer pour réduire le temps nécessaire à son exécution. Cependant, ce chapitre n’est qu’une introduction à la recherche de chemin. Si vous vous intéressez aux bonnes
=Labat FM.book Page 187 Vendredi, 19. juin 2009 4:01 16
Pathfinding : programmer les déplacements des personnages CHAPITRE 9
187
pratiques en matière d’optimisation d’algorithmes, l’Internet fourmille d’articles à ce sujet, consultez-les.
L’algorithme A* : compromis entre performance et pertinence Dans cette section, nous allons nous pencher sur le fonctionnement de l’algorithme et verrons un exemple d’utilisation. Vous serez ainsi capable de l’utiliser dans vos jeux.
Principe de l’algorithme Nous allons à présent travailler sur une carte représentant le niveau d’un jeu. Chaque case qui constitue la carte est appelée nœud. Le point de départ sera symbolisé par une case verte et le point d’arrivée, par une case rouge. Les cases blanches symbolisent des cases classiques, franchissables sans effort particulier. Les cases bleues symbolisent l’eau et sont plus difficiles à franchir que les cases classiques. Enfin, les cases grises symbolisent des murs parfaitement infranchissables. La figure 9-1 donne un exemple de ce type de carte. Figure 9-1
Le type de carte qui sera utilisé
Le travail de recherche de chemin s’effectue grâce à deux listes de nœuds : • une liste ouverte, qui contient les nœuds susceptibles de conduire le joueur au nœud de destination, c’est-à-dire les nœuds à vérifier ; • et une liste fermée, contenant les nœuds déjà traités qui composeront le chemin final. Intéressons-nous d’abord à la première phase de l’algorithme. Commencez par ajouter le point de départ à la liste fermée, puisque la solution passera forcément par lui. Intéressez-vous ensuite à tous les points voisins de ce point de départ et ajoutez-les également à la liste ouverte, tout en ignorant ceux qui sont infranchissables (les murs dans notre exemple).
=Labat FM.book Page 188 Vendredi, 19. juin 2009 4:01 16
188
Développement XNA pour la XBox et le PC
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 audessus du nœud de départ, il y a 15 déplacements horizontaux et un vertical. Pour le nœud à droite du nœud de départ, il y a 14 déplacements horizontaux et 2 verticaux. Nous pouvons donc utiliser n’importe lequel des deux nœuds pour continuer notre recherche. Choisissez-en un, retirez-le de la liste ouverte et ajoutez-le à la liste fermée. Puis répétez les opérations d’analyse et de détermination du meilleur nœud voisin, jusqu’à arriver au nœud de destination. Une fois à destination, remontez de nœud en nœud grâce au nœud parent que vous avez défini pour chacun d’entre eux, jusqu’à arriver au nœud de départ qui n’aura pas de parent. Attention Si, à un moment donné, la liste ouverte ne contient plus de nœud, mais que vous n’êtes pas encore arrivé au nœud de destination, c’est qu’il n’existe pas de chemin possible vers ce nœud. Figure 9-2
Analyse du second nœud et de ses voisins…
=Labat FM.book Page 189 Vendredi, 19. juin 2009 4:01 16
Pathfinding : programmer les déplacements des personnages CHAPITRE 9
189
Figure 9-3
… jusqu’au nœud de destination
Dans le calcul de l’estimation de la distance vers le nœud de destination, il est possible d’ajouter la notion de coût du passage sur un nœud. Le chemin déterminé, visible sur les figures précédentes, utilise cette notion. Le passage sur une case classique n’a pas de coût particulier, alors que le passage sur une case eau (bleue) a un coût de deux. Si vous supprimez la zone d’eau ou que vous diminuez son coût, l’algorithme préférera passer par cette zone (figure 9-4). On peut également imaginer la présence d’un pont au-dessus du cours d’eau : ainsi, en ajustant bien les coûts, l’algorithme préférera emprunter le pont plutôt que d’envoyer le personnage dans l’eau. Figure 9-4
Sans la zone d’eau, l’algorithme détermine un autre chemin
=Labat FM.book Page 190 Vendredi, 19. juin 2009 4:01 16
190
Développement XNA pour la XBox et le PC
Implanter l’algorithme dans un jeu de type STR Maintenant que vous connaissez les bases du fonctionnement de l’algorithme, cette section explique comment l’appliquer à un début de jeu de stratégie en temps réel (STR). La classe Tile et la carte
Créez un nouveau projet baptisé « ChapitreNeuf » et renommez la classe Game1 en ChapitreNeuf. Ajoutez la classe Sprite telle qu’elle était à la fin du chapitre 6, puis créez une classe dérivée de Sprite que vous appellerez Tile. Une carte est composée à l’écran de plusieurs objets de type Tile, il s’agit donc de « cases ». Il en existe trois types : • une case normale ; • une case représentant de l’eau ; • une case représentant un mur. Créez une énumération chargée de représenter ces trois cas. enum TileType { Wall = -1, Normal = 0, Water = 2, };
Il est également nécessaire d’ajouter une nouvelle paire de coordonnées (x, y) à cette classe. En effet, les coordonnées fournies par la classe Sprite représentent les coordonnées à l’écran or, ici, il faudra travailler avec les coordonnées sur une carte de jeu. Ces nouvelles coordonnées représenteront la position de l’objet Tile dans un tableau à deux dimensions, la valeur y étant la position sur la première dimension et la valeur x, la position sur la deuxième dimension. Chaque case sera représentée à l’écran par un carré de 32 ¥ 32 pixels qui sera coloré selon son type : gris si c’est un mur, bleu pour l’eau, vert pour le point de départ, rouge pour le point d’arrivée et enfin, orange pour les points composant le chemin retourné par l’algorithme. La classe Tile class Tile : Sprite { int x; public int X { get { return this.x; } set { this.x = value ; } } int y;
=Labat FM.book Page 191 Vendredi, 19. juin 2009 4:01 16
Pathfinding : programmer les déplacements des personnages CHAPITRE 9
191
public int Y { get { return this.y; } set { this.x = value ; } } TileType type; public TileType Type { get { return type; } } public Tile(int y, int x, byte type) : base(new Vector2(x * 32, y * 32)) { this.x = x; this.y = y; switch (type) { case 1: Color = Color.Gray; this.type = TileType.Wall; break; case 2: Color = Color.Blue; this.type = TileType.Water; break; default: this.type = TileType.Normal; break; } } }
Ajoutez ensuite une classe Map. Elle contiendra la liste des Tile à afficher dans un tableau à deux dimensions. Ce tableau sera initialisé et rempli dans le constructeur de la classe via un tableau de byte (0 pour une case normale, 1 pour un mur et 2 pour de l’eau). Bien sûr, la classe dispose d’une méthode Draw() qui va parcourir le tableau et appeler la méthode du même nom de chaque Tile. Pour terminer, la méthode ValidCoordinates () permettra de déterminer si une paire de coordonnées (x, y) correspond à un élément valide du tableau. La classe Map class Map { Tile[,] tileList; public Tile[,] TileList { get { return tileList; } set { tileList = value; } }
=Labat FM.book Page 192 Vendredi, 19. juin 2009 4:01 16
192
Développement XNA pour la XBox et le PC
public Map(byte[,] table) { tileList = new Tile[table.GetLength(0),table.GetLength(1)]; for (int y = 0; y < table.GetLength(0); y++) { for (int x = 0; x < table.GetLength(1); x++) { tileList[y, x] = new Tile(y, x, table[y, x]); } } } public void Draw(SpriteBatch spriteBatch) { foreach (Tile tile in tileList) { tile.Draw(spriteBatch); } } public bool ValidCoordinates(int x, int y) { if (x < 0) return false; if (y < 0) return false; if (x >= tileList.GetLength(1)) return false; if (y >= tileList.GetLength(0)) return false; return true; } }
Lier les nœuds aux cases : la distance de Manhattan
La prochaine classe à ajouter est celle des nœuds, appelez-la Node. Chaque nœud doit être lié à une case Tile, doit pouvoir avoir un nœud parent et enfin, et doit connaître l’estimation de la distance vers le nœud de destination. Nous utiliserons ici la distance de Manhattan. Dans ce calcul, n’oubliez pas d’ajouter le coût de la case (les valeurs étant fixées dans l’énumération TileType). La méthode GetPossibleNode() va chercher les nœuds voisins (uniquement sur les axes horizontaux et verticaux) qui sont des cases valides et ne sont pas des murs. Elle renvoie ensuite une collection List contenant les nœuds ainsi trouvés. La classe Node class Node { Tile tile;
=Labat FM.book Page 193 Vendredi, 19. juin 2009 4:01 16
Pathfinding : programmer les déplacements des personnages CHAPITRE 9
public Tile Tile { get { return tile; } } Node parent; public Node Parent { get { return parent; } set { parent = value; } } int estimatedMovement; public int EstimatedMovement { get { return estimatedMovement; } } public Node(Tile tile, Node parent, Tile destination) { this.tile = tile; this.parent = parent; this.estimatedMovement = Math.Abs(tile.X - destination.X) + Math.Abs(tile.Y ➥ - destination.Y) + (int)tile.Type; } public List GetPossibleNode(Map map, Tile destination) { List result = new List(); // Bottom if (map.ValidCoordinates(tile.X, tile.Y + 1) && map.TileList[tile.Y + 1, ➥ tile.X].Type != TileType.Wall) result.Add(new Node(map.TileList[tile.Y + 1, tile.X], this, ➥ destination)); // Right if (map.ValidCoordinates(tile.X + 1, tile.Y) && map.TileList[tile.Y, ➥ tile.X + 1].Type != TileType.Wall) result.Add(new Node(map.TileList[tile.Y, tile.X + 1], this, ➥ destination)); // Top if (map.ValidCoordinates(tile.X, tile.Y - 1) && map.TileList[tile.Y - 1, ➥ tile.X].Type != TileType.Wall) result.Add(new Node(map.TileList[tile.Y - 1, tile.X], this, ➥ destination)); // Left if (map.ValidCoordinates(tile.X - 1, tile.Y) && map.TileList[tile.Y, ➥ tile.X - 1].Type != TileType.Wall) result.Add(new Node(map.TileList[tile.Y, tile.X - 1], this, ➥ destination)); return result; } }
193
=Labat FM.book Page 194 Vendredi, 19. juin 2009 4:01 16
194
Développement XNA pour la XBox et le PC
Listes ouverte et fermée : la collection List
Il ne reste plus qu’une chose à préparer : une collection qui servira aux listes ouverte et fermée. Il s’agit ici de créer une collection dérivée de la collection List, l’intérêt étant de lui donner la possibilité de retourner un objet sans connaître son index, de savoir si un nœud est déjà présent dans la liste et enfin, d’effectuer une insertion dichotomique dans la liste. Qu’est-ce qu’une insertion dichotomique ? Dans le principe de l’algorithme, vous avez pu lire qu’à chaque itération, il faut choisir le nœud dont la distance avec le nœud de destination est la plus faible. Vous pouvez donc parcourir à chaque fois la liste ouverte à la recherche du nœud ayant la distance la plus faible ou alors entretenir une liste triée dans laquelle vous savez que le premier élément de la liste est toujours celui qui a la distance la plus faible. L’insertion dichotomique vous permet donc de placer cet élément au bon endroit. Voici l’algorithme en pseudo-code : Gauche ChapitreDix.SCREEN_WIDTH) Position = new Vector2(ChapitreDix.SCREEN_WIDTH - Texture.Width, ➥ Position.Y); if (Position.Y < 0) Position = new Vector2(Position.X, 0); if (Position.Y + Texture.Height > ChapitreDix.SCREEN_HEIGHT) Position = new Vector2(Position.X, ChapitreDix.SCREEN_HEIGHT ➥ Texture.Height); }
À vos claviers La structure qui a été retenue pour ce jeu se contente d’utiliser des classes dérivées de la classe Sprite. Cependant, vous pourriez très bien faire dériver la classe Player de la classe DrawableGameComponent, ou encore utiliser les interfaces IGameComponent, IUpdatable et IDrawable. N’hésitez pas à récrire ce mini-jeu exemple avec ces solutions, cela constitue un très bon entraînement.
=Labat FM.book Page 210 Vendredi, 19. juin 2009 4:01 16
210
Développement XNA pour la XBox et le PC
Dernière méthode de cette classe, ResetPosition () se chargera de placer le vaisseau du joueur au centre de la largeur de l’écran et en retrait de 10 % de sa hauteur. Cette méthode sera appelée en début de partie (après le chargement de la texture du vaisseau), ainsi qu’à chaque fois que le joueur aura perdu. public void ResetPosition() { Position = new Vector2(ChapitreDix.SCREEN_WIDTH / 2 - (Texture.Width / 2), 9 * ➥ (ChapitreDix.SCREEN_HEIGHT / 10) - Texture.Height); }
Créer les astéroïdes
À présent, vous allez vous occuper des astéroïdes. Tout d’abord, créez une texture ou récupérez-la sur Internet, par exemple sur http://www.cgtextures.com/. Comme vous le voyez à la figure 10-2, nous utilisons un simple disque marron. Figure 10-2
Des sphères parfaites se cachent peut-être dans l’espace…
Créez une classe Asteroid et ajoutez-lui un attribut speed de type Vector2 : il pourra y avoir plusieurs astéroïdes à l’écran, la position et la vitesse de chacun d’entre eux sera aléatoire. Vous fixez ces deux éléments dans une méthode privée Initialize (). Pour la génération de nombres aléatoires, vous utiliserez un objet statique de la classe Random et vous passerez à son constructeur le nombre de millisecondes à cet instant. static Random random = new Random(DateTime.Now.Millisecond); Avancé. Les nombres aléatoires Il faut savoir qu’en informatique, les nombres aléatoires ne le sont pas réellement. En fait, il est impossible de générer une suite de nombres réellement aléatoires, il faudrait plutôt les appeler nombres pseudo-aléatoires. La détermination de ces nombres se fait via l’utilisation d’une graine, c’est-à-dire d’une valeur de départ. Si la valeur de cette graine est toujours la même, les nombres générés par le programme seront toujours les mêmes. Pour éviter ce problème, vous pouvez faire varier la valeur de la graine en fonction du temps.
La méthode Next() de l’objet random attend comme paramètres une borne inférieure et une borne supérieure, puis retourne un entier. Initialement, le sprite devra se situer légèrement au-dessus de la fenêtre et n’importe où sur l’axe des abcisses. Pour la vitesse, en ce qui concerne l’axe X, l’astéroïde doit pouvoir aller vers la gauche comme vers la droite ; en ce qui concerne l’axe Y, il doit uniquement pouvoir aller vers le bas. private void Initialize() { Position = new Vector2(random.Next(0, ChapitreDix.SCREEN_WIDTH - Texture.Width), ➥ -Texture.Height); speed = new Vector2((float)random.Next(-7, 7) / 10, (float)random.Next(1, 7) ➥ / 10); }
=Labat FM.book Page 211 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
211
Vous pouvez à présent écrire le constructeur. Dans celui-ci, vous appellerez la méthode LoadContent () de la classe parente, ainsi que la méthode Initialize (). public Asteroid(ContentManager content) : base(Vector2.Zero) { base.LoadContent(content, "asteroid"); Initialize(); }
Dernière chose, la méthode Update () où vous mettrez à jour la position de l’astéroïde et, s’il sort de l’écran, où vous appellerez sa méthode Initialize(). public void Update(GameTime gameTime) { Position = new Vector2(Position.X + (speed.X * ➥ gameTime.ElapsedGameTime.Milliseconds), Position.Y + (speed.Y * ➥ gameTime.ElapsedGameTime.Milliseconds)); if (Position.X + Texture.Width < 0) Initialize(); if (Position.X > ChapitreDix.SCREEN_WIDTH) Initialize(); if (Position.Y > ChapitreDix.SCREEN_HEIGHT) Initialize(); }
Vous n’avez plus qu’à utiliser ces deux classes. Il n’y a rien de spécial à souligner en ce qui concerne la création du vaisseau du joueur. Vous stockerez les astéroïdes dans une collection List. Pour faire apparaître un astéroïde toutes les x secondes, utilisez un objet de type TimeSpan que vous remettrez à zéro à chaque ajout d’un nouvel objet à la liste. La classe de base du jeu complétée public class ChapitreDix : Microsoft.Xna.Framework.Game { public static int SCREEN_WIDTH = 512; public static int SCREEN_HEIGHT = 748; static int NEW_METEOR_TIME = 5; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Player playerShip; List asteroids = new List(); TimeSpan elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; public ChapitreDix() {
=Labat FM.book Page 212 Vendredi, 19. juin 2009 4:01 16
212
Développement XNA pour la XBox et le PC
graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferHeight = SCREEN_HEIGHT; graphics.PreferredBackBufferWidth = SCREEN_WIDTH; } protected override void Initialize() { playerShip = new Player(); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); playerShip.LoadContent(Content, "ship"); playerShip.ResetPosition(); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); playerShip.Update(gameTime); foreach (Asteroid asteroid in asteroids) asteroid.Update(gameTime); elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime; if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME) { asteroids.Add(new Asteroid(Content)); elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); playerShip.Draw(spriteBatch); foreach (Asteroid asteroid in asteroids)
=Labat FM.book Page 213 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
213
asteroid.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
Vous pouvez à présent compiler et exécuter le jeu (voir figure 10-3). Cependant, sans les collisions, ce n’est pas très intéressant d’y jouer… Figure 10-3
Les astéroïdes deviennent vite très nombreux
Établir une zone de collision autour des astéroïdes La méthode la plus simple pour tester si le vaisseau du joueur entre en collision avec un astéroïde consiste à encadrer chaque élément par un rectangle, puis à tester si les rectangles se coupent ou non. Commencez par ajouter une propriété à la classe Sprite afin que les classes Player et Asteroid puissent en bénéficier. Cette propriété devra créer un rectangle en fonction de la position du Sprite et de la taille de sa texture. Pour que la classe reste générique, si la variable sourceRectangle n’est pas nulle, utilisez-la plutôt que les dimensions de la texture.
=Labat FM.book Page 214 Vendredi, 19. juin 2009 4:01 16
214
Développement XNA pour la XBox et le PC
public Rectangle Rectangle { get { if (sourceRectangle == null) return new Rectangle((int)position.X, (int)position.Y, texture.Width, ➥ texture.Height); else return new Rectangle((int)position.X, (int)position.Y, ➥ sourceRectangle.Value.Width, sourceRectangle.Value.Height); } }
Cette portion de code aurait aussi pu s’écrire de la manière suivante : public Rectangle Rectangle { get { return new Rectangle((int)Position.X, (int)Position.Y, (sourceRectangle == null) ? Texture.Width : sourceRectangle.Value.Width, ➥ (sourceRectangle == null) ? Texture.Height : sourceRectangle.Value.Height); } } Avancé Opérateur ternaire L’exemple précédent utilise un élément du langage que vous n’avez encore jamais vu : l’opérateur ternaire. Il permet de résumer des blocs if en une seule ligne. Sa syntaxe est la suivante :
(test) ? valeur si vrai : valeur si faux
Modifiez ensuite la méthode Update () de la classe ChapitreDix pour qu’elle effectue le test entre les deux rectangles. Utilisez la méthode Intersects() d’un rectangle et passez l’autre rectangle en argument. Si les deux rectangles se superposent, le joueur a perdu : il faut réinitialiser sa position et vider la liste des astéroïdes. La liste se vide via la méthode Clear (). Attention, si vous supprimez un ou plusieurs éléments de la collection, vous devez sortir de la boucle qui est en train de l’énumérer. protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); playerShip.Update(gameTime); foreach (Asteroid asteroid in asteroids) { asteroid.Update(gameTime); if (playerShip.Rectangle.Intersects(asteroid.Rectangle)) { playerShip.ResetPosition();
=Labat FM.book Page 215 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
215
asteroids.Clear(); break; } } elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime; if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME) { asteroids.Add(new Asteroid(Content)); elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; } base.Update(gameTime); }
Vous pouvez tester le jeu à présent : la détection des collisions fonctionne et le jeu reprendra à zéro dès que le joueur percutera un astéroïde. En réalité, cette dernière phrase n’est pas juste : le jeu reprend à zéro lorsque le rectangle qui entoure le vaisseau du joueur entre en collision avec celui qui entoure un astéroïde, pourtant il n’y a pas forcément collision entre les deux éléments (figure 10-4). Figure 10-4
Il y a collision entre les rectangles mais pas entre les deux éléments
Comment savoir s’il y a vraiment collision entre deux éléments ? La réponse se situe au niveau des pixels… Si vous détectez une collision potentielle grâce à la méthode d’intersection 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
216
Développement XNA pour la XBox et le PC
Passons maintenant à l’écriture de la méthode CollisionPerPixel (dans le projet exemple, elle est rattachée à la classe ChapitreDix). La première chose à faire est de déterminer le rectangle contenant tous les pixels concernés par la collision entre les deux sprites. Une fois la position des quatre côtés du rectangle déterminée, utilisez-les pour parcourir le tableau de pixels. Attention, tous les pixels sont classés de manière linéaire dans le tableau puisque celui-ci ne comporte qu’une dimension. Pour chacun des pixels de cette zone, vérifiez la composante alpha (la transparence). Si elle n’est pas nulle pour les deux sprites, alors les deux sprites ne sont pas totalement transparents et il y a collision. protected bool CollisionPerPixels(Sprite spriteA, Sprite spriteB) { int top = Math.Max(spriteA.Rectangle.Top, spriteB.Rectangle.Top); int bottom = Math.Min(spriteA.Rectangle.Bottom, spriteB.Rectangle.Bottom); int left = Math.Max(spriteA.Rectangle.Left, spriteB.Rectangle.Left); int right = Math.Min(spriteA.Rectangle.Right, spriteB.Rectangle.Right); for (int y = top; y < bottom; y++) { for (int x = left; x < right; x++) { Color colorA = spriteA.TextureData[(x (y - spriteA.Rectangle.Top) Color colorB = spriteB.TextureData[(x (y - spriteB.Rectangle.Top)
spriteA.Rectangle.Left) + * spriteA.Rectangle.Width]; spriteB.Rectangle.Left) + * spriteB.Rectangle.Width];
if (colorA.A != 0 && colorB.A != 0) return true; } } return false; }
Reprenez la méthode et ajoutez l’appel à la fonction CollisionPerPixels() en plus de la méthode de détection qui utilise les rectangles. Langage C# Opérateur && Lorsque vous utilisez l’opérateur && de la manière suivante :
test_A && test_B Si le résultat de test_A est faux, test_B ne sera même pas exécuté. Ainsi, le test qui descend au niveau de détails des pixels ne sera exécuté que si le test plus large avec les rectangles a détecté une collision potentielle.
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
=Labat FM.book Page 217 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
217
playerShip.Update(gameTime); foreach (Asteroid asteroid in asteroids) { asteroid.Update(gameTime); if (playerShip.Rectangle.Intersects(asteroid.Rectangle) && ➥ CollisionPerPixels(playerShip, asteroid)) { playerShip.ResetPosition(); asteroids.Clear(); break; } } elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime; if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_METEOR_TIME) { asteroids.Add(new Asteroid(Content)); elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; } base.Update(gameTime); }
Cette fois-ci, si vous testez le jeu, vous remarquez qu’il est possible d’effleurer les astéroïdes sans problèmes de collisions (figure 10-5). Il ne vous reste plus qu’à compléter le jeu en ajoutant par exemple un système de gestion du score. Figure 10-5
La détection de collision est maintenant plus précise
Simuler un environnement spatial : la gestion de la physique Votre simulation spatiale n’est pas encore parfaite : en effet, les déplacements des vaisseaux ne semblent pas vraiment naturels et les astéroïdes n’entrent pas en collision entre eux… Dans la deuxième partie de ce chapitre, vous allez apprendre à utiliser un moteur physique pour simuler un environnement ressemblant à l’espace.
Choisir un moteur physique En fait, le moteur physique est le module du jeu qui s’occupe du mouvement des objets, de la manière dont ils interagissent les uns avec les autres (par exemple les collisions), ou encore des comportements spéciaux qu’ils peuvent avoir (rebonds, frictions, déformations, etc.). Certains jeux portent un intérêt énorme à la physique. Par exemple, dans Half Life 2, ou encore dans Portal (ces deux jeux utilisent le même moteur physique Havok), le joueur
=Labat FM.book Page 218 Vendredi, 19. juin 2009 4:01 16
218
Développement XNA pour la XBox et le PC
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 modifications) dans tous les contextes ;
=Labat FM.book Page 219 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
219
• gratuit et open source uniquement pour les jeux non commerciaux ; • gratuit et closed source dans tous les contextes ; • payant et closed source. Licences Il existe un tas d’autres licences (elles portent d’ailleurs toutes un nom), les nuances entre elles étant parfois très subtiles : identifiez donc bien votre besoin lors du choix d’un moteur.
Dans ce chapitre, vous allez faire vos premiers pas avec le moteur FarseerPhysics. Ce moteur est gratuit, open source et directement compatible avec XNA.
Télécharger et installer FarseerPhysics Le moteur physique FarseerPhysics est, au moment de l’écriture de ce livre, disponible en version 2.0.1. Le projet a été initialement lancé par Jeff Weber, mais il est à présent maintenu par une équipe de trois personnes. Il est disponible pour XNA et Silverlight (la technologie Microsoft concurrente d’Adobe Flash), mais propose aussi des classes indépendantes de toute plate-forme graphique. 1. Rendez-vous sur la page du projet sur CodePlex, la plate-forme de Microsoft pour les projets open source : http://www.codeplex.com/FarseerPhysics. Accédez à la page de téléchargement en cliquant sur l’onglet Releases et choisissez le projet pour XNA (figure 10-7). Figure 10-7
La version du moteur adaptée à une utilisation avec XNA
2. Les développeurs proposent aussi une version du moteur avec des exemples d’utilisation 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
220
Développement XNA pour la XBox et le PC
Figure 10-8
Une grande quantité de cubes soumis à la gravité
Figure 10-9
Le moteur sait gérer une multitude de collisions sans que les performances en pâtissent trop
=Labat FM.book Page 221 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
221
3. Que vous ayez choisi de télécharger le moteur avec ou sans exemples, l’archive contiendra un projet FarseerPhysics.csproj. Ajoutez ce projet à la solution (figure 10-10) et générez-le (figure 10-11).
Figure 10-10
Ajout du projet de FarseerPhysics à la solution Figure 10-11
Génération du projet FarseerPhysics
4. Créez ensuite un projet nommé ChapitreDix_2. Pour pouvoir utiliser le moteur dans ce projet, vous devez ajouter une référence vers le projet FarseerPhysics. Cliquez droit sur la section référence du projet dans l’explorateur de solution, puis cliquez sur Ajouter une référence… Dans la fenêtre qui s’ouvre, cliquez sur l’onglet Projets et choisissez dans la liste celui nommé FarseerPhysics (figure 10-12).
Figure 10-12
Ajout d’une référence vers un autre projet
=Labat FM.book Page 222 Vendredi, 19. juin 2009 4:01 16
222
Développement XNA pour la XBox et le PC
5. La dernière chose à faire est d’ajouter une directive using. using FarseerGames.FarseerPhysics;
Prise en main du moteur physique Une nouvelle fois, ajoutez au projet les fichiers IKeyboardService.cs, KeyboardService.cs, ServiceHelper.cs et Sprite.cs. Créez les classes Player et Asteroid qui dériveront de la classe Sprite. L’utilisation du moteur FarseerPhysics repose sur la classe PhysicsSimulator qui dispose de deux constructeurs : le premier n’attend aucun paramètre, alors que le second attend un objet de type Vector2. Cet objet correspond à la gravité à appliquer sur les axes X et Y. Donc, si vous utilisez le constructeur sans arguments, il n’y aura pas de gravité ! Vous devrez appeler régulièrement la méthode Update() du simulateur pour que celui-ci mette à jour tous les éléments qu’il gère. Cette méthode attend comme paramètre un intervalle de temps. Pour se rapprocher des calculs physiques habituels, passez-lui un temps en secondes. Ce temps est récupérable en utilisant la propriété ElapsedGameTime.Milliseconds que vous diviserez par mille (la propriété ElapsedGameTime.Seconds étant un entier, elle serait imprécise). Enfin, passez l’objet de type PhysicsSimulator au constructeur des classes Player et Asteroid. Ci-dessous, vous retrouvez le code complet de la classe ChapitreDix_2. Première classe de test du moteur physique public class ChapitreDix_2 : Microsoft.Xna.Framework.Game { public static int SCREEN_WIDTH = 512; public static int SCREEN_HEIGHT = 748; static int NEW_ASTEROID_TIME = 5; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; PhysicsSimulator physicsSimulator; Player ship; List asteroids = new List(); TimeSpan elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; public ChapitreDix_2() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferHeight = SCREEN_HEIGHT; graphics.PreferredBackBufferWidth = SCREEN_WIDTH; }
=Labat FM.book Page 223 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
protected override void Initialize() { physicsSimulator = new PhysicsSimulator(); ship = new Player(physicsSimulator); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); ship.LoadContent(Content, "ship2"); ship.ResetPosition(); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); physicsSimulator.Update(gameTime.ElapsedGameTime.Seconds * 0.001f); ship.Update(gameTime); foreach (Asteroid asteroid in asteroids) { asteroid.Update(gameTime); } elapsedTimeSinceLastNewAsteroid += gameTime.ElapsedGameTime; if (elapsedTimeSinceLastNewAsteroid.Seconds >= NEW_ASTEROID_TIME) { asteroids.Add(new Asteroid(physicsSimulator, Content)); elapsedTimeSinceLastNewAsteroid = TimeSpan.Zero; } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); ship.Draw(spriteBatch); foreach (Asteroid asteroid in asteroids) asteroid.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
223
=Labat FM.book Page 224 Vendredi, 19. juin 2009 4:01 16
224
Développement XNA pour la XBox et le PC
À présent, vous devez adapter la classe Player du début de ce chapitre à l’utilisation du moteur physique. Celui-ci ne traitera pas directement une texture ni un sprite, il travaillera avec des objets de type Body (des corps). Vous devez donc créer un corps pour chaque élément qui devra être traité par FarseerPhysics. La classe Body se situe dans l’espace de noms FarseerGames.FarseerPhysics.Dynamics. La création d’un corps se fait en utilisant la classe BodyFactory (espace de noms FarseerGames .FarseerPhysics.Factories). La méthode à utiliser dépend de la forme que vous désirez donner à votre corps. Dans le cas du vaisseau du joueur, utilisez une forme carrée grâce à la méthode CreateRectangleBody(). Tableau 10-1 Paramètres de la méthode CreateRectangleBody Paramètre
Description
PhysicsSimulator physicsSimulator
Ce paramètre est optionnel. Si vous l’utilisez, le corps sera automatiquement ajouté au simulateur physique.
Float width
Largeur du corps.
Float height
Hauteur du corps.
Float mass
Masse du corps.
Un corps possède ses propres propriétés position et rotation. Le sprite est la représentation graphique du vaisseau et le corps, sa représentation physique. Pour ne pas avoir de décalage entre les deux, vous mettez à jour la position et l’angle de rotation du sprite en fonction des propriétés du corps. En ce qui concerne la rotation du corps, le point d’origine est pris au milieu du corps, pensez donc à faire de même en ce qui concerne le sprite. Vous pouvez appliquer une force sur un corps simplement grâce à la méthode ApplyForce (). Elle attend comme paramètre un objet Vector2 qui contient les valeurs à appliquer sur les axes X et Y. Pour appliquer une force sur le vaisseau et que celui-ci se dirige dans la bonne direction, utilisez les fonctions mathématiques Sin() et Cos() à partir de son angle de rotation en radian (rappelez-vous du cercle trigonométrique !). Les quatre derniers tests de la méthode Update() permettent au vaisseau de disparaître d’un côté de l’écran pour réapparaître de l’autre. Le vaisseau du joueur utilisant le moteur physique class Player : Sprite { Body body; public Player(PhysicsSimulator physicsSimulator) : base(Vector2.Zero) { body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1); }
=Labat FM.book Page 225 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
225
public void Update(GameTime gameTime) { Position = body.Position; Rotation = body.Rotation; if (ServiceHelper.Get().IsKeyDown(Keys.Left)) body.Rotation -= 0.05f; if (ServiceHelper.Get().IsKeyDown(Keys.Right)) body.Rotation += 0.05f; if (ServiceHelper.Get().IsKeyDown(Keys.Up)) body.ApplyForce(new Vector2((float)Math.Sin(body.Rotation) * 100, ➥ (float)Math.Cos(body.Rotation) * -100)); if (ServiceHelper.Get().IsKeyDown(Keys.Down)) body.ApplyForce(new Vector2((float)Math.Sin(body.Rotation) * -100, ➥ (float)Math.Cos(body.Rotation) * 100)); if (body.Position.X > ChapitreDix_2.SCREEN_WIDTH + (2 * Texture.Width)) body.Position = new Vector2(-Texture.Width, body.Position.Y); if (body.Position.X < 0 - (2 * Texture.Width)) body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH + Texture.Width, ➥ body.Position.Y); if (body.Position.Y > ChapitreDix_2.SCREEN_HEIGHT + (2 * Texture.Height)) body.Position = new Vector2(body.Position.X, -Texture.Height); if (body.Position.Y < 0 - (2 * Texture.Height)) body.Position = new Vector2(body.Position.X, ChapitreDix_2.SCREEN_HEIGHT ➥ + Texture.Height); } public void ResetPosition() { body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH / 2 - (Texture.Width ➥ / 2), 9 * (ChapitreDix_2.SCREEN_HEIGHT / 10) - Texture.Height); Position = body.Position; body.Rotation = 0; Rotation = body.Rotation; Origin = new Vector2(Texture.Width / 2, Texture.Height / 2); } }
En ce qui concerne la classe Asteroid, il faut appliquer deux forces. La première utilise la méthode ApplyForce() que nous venons de voir (la direction sera aléatoire). La seconde utilise la méthode ApplyForceAtLocalPoint() qui, comme son nom l’indique, vous permet d’appliquer une force à un point précis de votre corps. Ce point, déterminé par un objet
=Labat FM.book Page 226 Vendredi, 19. juin 2009 4:01 16
226
Développement XNA pour la XBox et le PC
de type Vector2 est donc le deuxième paramètre attendu par la méthode. Dans le cas présent, il est choisi aléatoirement de manière à ce que la rotation ainsi subie par le corps ne soit pas la même pour tous les astéroïdes. Cette fois encore, n’oubliez pas de toujours coordonner position et rotation entre le sprite et le corps ! Les astéroïdes utilisent eux aussi le moteur physique class Asteroid : Sprite { static Random random = new Random(DateTime.Now.Millisecond); Body body; public Asteroid(PhysicsSimulator physicsSimulator, ContentManager content) : base(Vector2.Zero) { base.LoadContent(content, "asteroid2"); body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1); Origin = new Vector2(Texture.Width / 2, Texture.Height / 2); Initialize(); } private void Initialize() { body.Position = new Vector2(random.Next(0, ChapitreDix_2.SCREEN_WIDTH ➥ Texture.Width), -Texture.Height); body.ApplyForce(new Vector2(random.Next(-3, 3) * 1000, random.Next(-3, 3) * ➥ 1000)); body.ApplyForceAtLocalPoint(new Vector2(random.Next(-3, 3) * 100, ➥ random.Next(-3, 3) * 100), new Vector2(random.Next(0, Texture.Width), ➥ random.Next(0, Texture.Height))); Position = body.Position; Rotation = body.Rotation; } public void Update(GameTime gameTime) { Position = body.Position; Rotation = body.Rotation; if (body.Position.X > ChapitreDix_2.SCREEN_WIDTH + (2 * Texture.Width)) body.Position = new Vector2(-Texture.Width, body.Position.Y); if (body.Position.X < 0 - (2 * Texture.Width)) body.Position = new Vector2(ChapitreDix_2.SCREEN_WIDTH + Texture.Width, ➥ body.Position.Y); if (body.Position.Y > ChapitreDix_2.SCREEN_HEIGHT + (2 * Texture.Height)) body.Position = new Vector2(body.Position.X, -Texture.Height); if (body.Position.Y < 0 - (2 * Texture.Height))
=Labat FM.book Page 227 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
227
body.Position = new Vector2(body.Position.X, ChapitreDix_2.SCREEN_HEIGHT ➥ + Texture.Height); } }
Maintenant, testez le jeu. L’inertie du vaisseau et les astéroïdes qui dérivent sont vraiment bien rendus. Au passage, vous n’avez qu’à améliorer l’image du vaisseau et celle des astéroïdes pour avoir un rendu plus old school (figure 10-13). Figure 10-13
La nouvelle version du jeu… sans les collisions !
Les collisions avec FarseerPhysics Cette nouvelle version du jeu doit faire face à un nouveau problème de taille : il n’y a plus de gestion des collisions ! Ce n’est pas grave, vous allez maintenant apprendre à les gérer avec le moteur physique. Les collisions entre astéroïdes
Pour gérer les collisions, FarseerPhysics utilise encore un autre type d’objet : les formes géométriques, Geom. Ces objets sont créés grâce à la classe GeomFactory et la méthode correspondant à la forme que vous voulez créer. Dans le cas des astéroïdes, de forme carrée, vous utiliserez la méthode CreateRectangleGeom ().
=Labat FM.book Page 228 Vendredi, 19. juin 2009 4:01 16
228
Développement XNA pour la XBox et le PC
Tableau 10-2 Paramètres de la méthode CreateRectangleGeom Paramètre
Description
PhysicsSimulator physicsSimulator
Ce paramètre est optionnel. Si vous l’utilisez, la forme géométrique sera automatiquement ajoutée au simulateur physique.
Body body
Corps concerné.
Float width
Largeur de la forme.
Float height
Hauteur de la forme.
Pour activer les collisions, définissez la propriété CollisionResponseEnabled de la forme géométrique à true. Vous pouvez indiquer le coefficient de réponse au choc (la force qui sera appliquée au corps qui entre en collision) grâce à la propriété ResitutionCoefficient. La portion de code ci-dessous présente le constructeur de la classe Asteroid qui crée maintenant une forme géométrique et active les collisions. public Asteroid(PhysicsSimulator physicsSimulator, ContentManager content) : base(Vector2.Zero) { base.LoadContent(content, "asteroid2"); body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1); Origin = new Vector2(Texture.Width / 2, Texture.Height / 2); GeomFactory.Instance.CreateRectangleGeom(physicsSimulator, body, 32, ➥ 32).CollisionResponseEnabled = true; Initialize(); }
Vous pouvez relancer le jeu : à présent, les astéroïdes dérivent et entrent doucement en collision. Les collisions avec le joueur
Les choses vont être un peu plus complexes pour la classe Player. En effet, dès que le vaisseau du joueur entrera en collision avec un autre élément, vous ne devrez pas le faire rebondir, mais lui faire recommencer le jeu à zéro. Pour cela, la classe Geom met à votre disposition l’événement OnCollision. Un événement se produit quand quelque chose se passe dans un programme : le clic d’un utilisateur sur un bouton, une collision entre deux entités, etc. Un événement doit être traité avec une fonction particulière appelée event handler, qui doit avoir une signature bien précise. Vous reliez l’événement à cette fonction en utilisant un délégué (vous les avez déjà rencontrés lors de l’étude des méthodes asynchrones). L’abonnement à l’événement se fait donc de la manière suivante : public Player(PhysicsSimulator physicsSimulator) : base(Vector2.Zero) { body = BodyFactory.Instance.CreateRectangleBody(physicsSimulator, 32, 32, 1);
=Labat FM.book Page 229 Vendredi, 19. juin 2009 4:01 16
Collisions et physique : créer un simulateur de vaisseau spatial CHAPITRE 10
229
GeomFactory.Instance.CreateRectangleGeom(physicsSimulator, body, 32, ➥ 32).OnCollision += new Geom.CollisionEventHandler(CollisionOccurs); }
Il ne vous reste plus qu’à ajouter la fonction CollisionOccurs(), qui est appelée dès qu’un événement a lieu. Nous avons vu sa signature avec IntelliSense au moment de la création du délégué (figure 10-14).
Figure 10-14
Signature de la fonction à créer
private bool CollisionOccurs(Geom geomA, Geom geomB, ContactList contactList) { return true; }
La fonction est encore incomplète, le temps de réfléchir un peu aux traitements à effectuer… Si le vaisseau entre en collision avec un astéroïde, il a perdu : il faut le remettre à sa position de départ et supprimer tous les astéroïdes. Il n’y a aucun problème pour appeler la fonction ResetPosition() de la classe Player. Cependant, vous n’avez pas accès à la liste des astéroïdes. Vous allez donc devoir vous-même faire parvenir un événement à la classe ChapitreDix_2 pour qu’elle s’occupe de vider la liste. Commencez par écrire le délégué et créer l’événement. public delegate void ShipHasExplodedEventHandler(); public event ShipHasExplodedEventHandler ShipHasExploded;
Puis, appelez l’événement dans la fonction CollisionOccurs(). private bool CollisionOccurs(Geom geomA, Geom geomB, ContactList contactList) { ResetPosition(); ShipHasExploded(); return true; }
Ensuite, dans la classe ChapitreDix_2, abonnez-vous à l’événement et écrivez la fonction qui videra la liste. protected override void Initialize() { physicsSimulator = new PhysicsSimulator(); ship = new Player(physicsSimulator); ship.ShipDestroyed += new Player.ShipDestroyedEventHandler(ship_ ➥ ShipHasExploded); base.Initialize(); }
=Labat FM.book Page 230 Vendredi, 19. juin 2009 4:01 16
230
Développement XNA pour la XBox et le PC
void ship_ShipHasExploded() { asteroids.Clear(); }
Vous pouvez maintenant tester le jeu et vous amuser avec !
En résumé Dans ce chapitre, vous avez découvert : • comment gérer les collisions en 2D avec des rectangles ; • comment gérer les collisions en 2D au niveau des pixels ; • ce qu’est un moteur physique, et une liste de moteurs utilisables en C# ; • comment utiliser le moteur FarseerPhysics.
=Labat FM.book Page 231 Vendredi, 19. juin 2009 4:01 16
11 Le mode multijoueur
Pong ou Tennis for Two, les premiers jeux vidéo, étaient déjà des jeux multijoueurs. Ainsi, le mode multijoueur, qu’il s’agisse de jouer en coopération ou de s’affronter sur un seul et même écran, existe depuis le début des jeux vidéo, et est bien entendu toujours utilisé de nos jours. Avec le temps, les jeux multijoueurs ont évolué et se sont mis à utiliser les communications réseau, reliant des stations de jeux plus ou moins éloignées (sur le même réseau local ou via Internet). Dans ce chapitre, nous allons développer un jeu qui utilise le scrolling et qui sera d’abord uniquement jouable en solo, puis jouable à deux sur le même écran, et enfin à deux via le réseau.
Jouer à plusieurs sur le même écran Il existe deux types de jeux vidéo qui offrent une expérience multijoueur sur le même écran : ceux où les joueurs apparaissent les uns à côté des autres dans la même vue et ceux où l’écran est partagé en plusieurs parties. Dans cette première partie, nous allons voir comment créer un jeu multijoueur utilisant le principe du partage d’écran. Un jeu en écran partagé, aussi appelé jeu en écran « splitté » (de l’anglais to split, qui signifie séparer), est un jeu où l’écran est divisé en plusieurs zones, généralement de même taille, chacune étant réservée à un joueur.
=Labat FM.book Page 232 Vendredi, 19. juin 2009 4:01 16
232
Développement XNA pour la XBox et le PC
Avec XNA, la division de l’écran en plusieurs parties se fera grâce à des objets de type Viewport. Il est possible de fixer la taille de ces objets, ainsi que leur point d’origine, chacun d’entre eux disposera de son propre repère cartésien.
Du mode solo au multijoueur : la gestion des caméras Au chapitre 3, nous avons vu ce que sont les jeux dits tile-based et nous les avons employés pour l’application de l’algorithme A* au chapitre 9. Dans ce chapitre, nous irons encore plus loin avec ce type de jeu et nous allons créer un système de caméra, le but étant de ne pas afficher toute la carte à l’écran, mais d’obtenir un effet de scrolling (en français, défilement). Nous avons déjà abordé le scrolling au chapitre 6 : il s’agit de faire défiler l’écran sur une carte en fonction des déplacements du joueur.
Créer un jeu solo avec effet de scrolling Commencez par créer un nouveau projet et récupérez, des précédents projets, les fichiers IKeyboardService.cs, KeyboardService.cs, ServiceHelper.cs, Map.cs, Sprite.cs et Tile.cs. Tout d’abord, modifiez l’interface IKeyboardService et ajoutez-lui la signature d’une méthode KeyHasBeenPressed. Le but de cette méthode est de détecter si le joueur a pressé une touche, puis l’a relâchée. Ainsi, le personnage ne se déplacera d’une case qu’à chaque fois que le joueur pressera une touche. Modifiez ensuite la classe KeyboardService de manière à ce qu’elle prenne en compte cette nouvelle méthode. interface IKeyboardService { bool IsKeyDown(Keys key); bool KeyHasBeenPressed(Keys key); }
Il n’y a aucune difficulté ici, d’autant plus que vous avez déjà fait la même chose dans un précédent chapitre pour le bouton gauche de la souris. class KeyboardService : GameComponent, IKeyboardService { KeyboardState lastKBState; KeyboardState KBState; public KeyboardService(Game game) : base(game) { ServiceHelper.Add(this); } public bool IsKeyDown(Keys key) { return KBState.IsKeyDown(key); }
=Labat FM.book Page 233 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
233
public bool KeyHasBeenPressed(Keys key) { return lastKBState.IsKeyDown(key) && KBState.IsKeyUp(key); } public override void Update(GameTime gameTime) { lastKBState = KBState; KBState = Keyboard.GetState(); base.Update(gameTime); } }
Intéressez-vous maintenant au système de caméra. La figure 11-1 représente votre objectif. Vous avez une carte (celle représentée ici est tirée du chapitre 9), et le carré aux bords noirs autour du joueur représente son champ de vision, c’est-à-dire la caméra. Figure 11-1
Une vision partielle d’une carte grâce à une caméra
Sur cet exemple, le joueur voit l’environnement qui l’entoure sur une distance de deux cases, la caméra est donc un carré de 5 ¥ 5 cases. La position de la caméra sur la carte sera déterminée par un couple de coordonnées (x, y). Vous pouvez donc utiliser un objet de type Rectangle qui dispose de toutes ces caractéristiques. Le sprite représentant le joueur doit toujours être placé au milieu de l’écran. Lorsque le joueur appuie sur les touches de déplacement, ce n’est pas la position à l’écran du sprite qui est modifiée, mais sa position sur la carte et les coordonnées de l’origine de la caméra. Modifiez maintenant la méthode Draw() de la classe Map afin qu’elle ne dessine que les cases qui sont dans le champ de vision de la caméra. Parcourez le tableau tileList en bouclant sur les dimensions de l’objet Rectangle, puis positionnez les sprites à l’écran en fonction de leur position dans le rectangle de la caméra et appelez leur méthode Draw().
=Labat FM.book Page 234 Vendredi, 19. juin 2009 4:01 16
234
Développement XNA pour la XBox et le PC
La classe Map modifiée class Map { Tile[,] tileList; public Tile[,] TileList { get { return tileList; } set { tileList = value; } } public Map(byte[,] table) { tileList = new Tile[table.GetLength(0),table.GetLength(1)]; for (int y = 0; y < table.GetLength(0); y++) { for (int x = 0; x < table.GetLength(1); x++) { tileList[y, x] = new Tile(y, x, table[y, x]); } } } public void Draw(SpriteBatch spriteBatch, Rectangle camera) { for (int y = camera.Y; y < camera.Y + camera.Height; y++) { for(int x = camera.X; x < camera.X + camera.Width; x++) { tileList[y, x].Position = new Vector2((x - camera.X) * 32, (y ➥ camera.Y) * 32); tileList[y, x].Draw(spriteBatch); } } } public bool ValidCoordinates(int x, int y) { if (x < 0) return false; if (y < 0) return false; if (x >= tileList.GetLength(1)) return false; if (y >= tileList.GetLength(0)) return false; return true; } }
=Labat FM.book Page 235 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
235
Passez à présent à la classe principale du projet où vous utiliserez ces nouvelles fonctions. Déclarez un objet Map, un objet Tile qui représentera le héros et enfin un objet Rectangle pour la caméra. Attention avec le tableau de byte lorsque vous instanciez l’objet Map. En effet, pour éviter qu’une exception soit levée lorsque le joueur est proche des bords de la carte (puisque la méthode Draw() de l’objet Map essaiera d’accéder à des index qui n’existent pas dans le tableau), placez des murs sur les bords de la carte. N’oubliez pas non plus de placer le sprite représentant le joueur au milieu de l’écran. Lorsque vous captez une entrée utilisateur, vérifiez si la case vers laquelle il souhaite se déplacer est un mur ou non. Si c’en est un, le déplacement ne doit pas avoir lieu. Enfin, dans la méthode Draw(), pensez à dessiner le joueur après avoir dessiné la carte. La classe principale du mini-jeu public class Chapitre11 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Map map; Tile herosA; Rectangle cameraA; public Chapitre11() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferWidth = 160; graphics.PreferredBackBufferHeight = 160; } protected override void Initialize() { map = new Map(new byte[,] { {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, {0, 1, 0, 0, 0, 0, 0, 0, 2, 2, {0, 1, 0, 0, 0, 0, 0, 0, 2, 2, {0, 1, 0, 0, 0, 0, 0, 0, 0, 2, {0, 1, 3, 0, 0, 0, 0, 0, 0, 0, {0, 1, 3, 3, 0, 0, 0, 0, 0, 0, {0, 1, 3, 3, 0, 0, 0, 0, 0, 0, {0, 1, 3, 0, 0, 0, 3, 3, 0, 0, {0, 1, 3, 0, 3, 3, 3, 3, 3, 0, {0, 1, 3, 3, 3, 3, 3, 3, 3, 3, {0, 1, 3, 3, 3, 3, 3, 3, 3, 3, {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, });
0, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 0,
0, 1, 1, 0, 1, 1, 2, 2, 2, 2, 2, 0, 1, 0,
0, 1, 0, 0, 0, 1, 0, 2, 2, 2, 2, 2, 1, 0,
0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}
=Labat FM.book Page 236 Vendredi, 19. juin 2009 4:01 16
236
Développement XNA pour la XBox et le PC
cameraA = new Rectangle(0, 0, 5, 5); herosA = new Tile(2, 2, 4); herosA.Position = new Vector2(64, 64); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); foreach (Tile tile in map.TileList) { tile.LoadContent(Content, "tile"); } herosA.LoadContent(Content, "tile"); } protected override void Update(GameTime gameTime) { if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Up)) { if (map.TileList[herosA.Y - 1, herosA.X].Type >= 0) { cameraA.Y -= 1; herosA.Y -= 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Right)) { if (map.TileList[herosA.Y, herosA.X + 1].Type >= 0) { cameraA.X += 1; herosA.X += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Down)) { if (map.TileList[herosA.Y + 1, herosA.X].Type >= 0) { cameraA.Y += 1; herosA.Y += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Left)) { if (map.TileList[herosA.Y, herosA.X - 1].Type >= 0) { cameraA.X -= 1; herosA.X -= 1; } } base.Update(gameTime); }
=Labat FM.book Page 237 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
237
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); map.Draw(spriteBatch, cameraA); herosA.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
Avant de compiler le projet, ajoutez le sprite qui sera utilisé pour chacune des cases (ici une image blanche de 32 ¥ 32 pixels). Vous pouvez ensuite l’essayer et vous amuser avec le scrolling. Figure 11-2
Votre jeu en mode solo
Adapter les caméras au multijoueur À présent, nous allons modifier le jeu de manière à ce qu’il puisse accueillir deux joueurs en simultané sur le même écran. Dans le code source de cet ouvrage, le projet correspondant est nommé Chapitre 11_2. La première chose à faire est de vous occuper des objets Viewport. Créez-en deux, un pour chaque joueur. Ici, ils seront appelés viewportA pour la portion supérieure de l’écran et viewportB pour la portion inférieure de l’écran. Spécifiez les dimensions des deux vues en définissant leurs propriétés Width et Height. Dans le programme exemple lié à ce chapitre, la largeur d’une vue est la même que la largeur de la fenêtre de jeu, et sa hauteur correspond à la moitié de celle de la fenêtre de jeu. Pensez donc à augmenter la hauteur de la fenêtre de jeu si vous voulez que les deux vues soient des carrés. Enfin, décalez la deuxième vue en hauteur grâce à sa propriété Y. Les deux vues seront donc alignées verticalement ; pour les aligner horizontalement, vous modifierez simplement la propriété X. viewportA.Width = graphics.PreferredBackBufferWidth; viewportA.Height = graphics.PreferredBackBufferHeight / 2;
=Labat FM.book Page 238 Vendredi, 19. juin 2009 4:01 16
238
Développement XNA pour la XBox et le PC
viewportB.Y = graphics.PreferredBackBufferHeight / 2; viewportB.Width = graphics.PreferredBackBufferWidth; viewportB.Height = graphics.PreferredBackBufferHeight / 2;
Déclarez un nouvel objet Tile qui représentera le deuxième joueur, ainsi qu’un nouvel objet Rectangle pour la caméra du deuxième joueur et faites-les bouger lorsque des touches du clavier seront utilisées (ici les touches Z-Q-S-D). Xbox 360 Dans l’exemple de jeu qui vous est proposé ici, les deux joueurs utilisent le clavier. Vous pouvez modifier le projet pour qu’il soit utilisable avec les manettes de la Xbox 360, vous différencierez les entrées utilisateur grâce à l’énumération PlayerIndex. Sur Xbox 360, les manettes peuvent être complétées par des claviers. L’état de ces claviers se récupère de la manière suivante :
Keyboard.GetState(PlayerIndex.Two);
Enfin, vous n’avez plus qu’à dessiner dans les deux vues. Pour vous placer sur une vue, modifiez la propriété Viewport de l’objet GraphicsDevice. Le dessin des sprites se fait de manière classique. La classe principale du jeu multijoueur public class Chapitre11_2 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Map map; Tile herosA; Tile herosB; Rectangle cameraA; Rectangle cameraB; Viewport viewportA; Viewport viewportB; public Chapitre11_2() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); graphics.PreferredBackBufferWidth = 160; graphics.PreferredBackBufferHeight = 320; } protected override void Initialize() {
=Labat FM.book Page 239 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
map = new Map(new byte[,] {0, 0, 0, 0, 0, 0, 0, {0, 1, 1, 1, 1, 1, 1, {0, 1, 0, 0, 0, 0, 0, {0, 1, 0, 0, 0, 0, 0, {0, 1, 0, 0, 0, 0, 0, {0, 1, 3, 0, 0, 0, 0, {0, 1, 3, 3, 0, 0, 0, {0, 1, 3, 3, 0, 0, 0, {0, 1, 3, 0, 0, 0, 3, {0, 1, 3, 0, 3, 3, 3, {0, 1, 3, 3, 3, 3, 3, {0, 1, 3, 3, 3, 3, 3, {0, 1, 1, 1, 1, 1, 1, {0, 0, 0, 0, 0, 0, 0, });
{ 0, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 1, 0,
0, 1, 2, 2, 0, 0, 0, 0, 0, 3, 3, 3, 1, 0,
0, 1, 2, 2, 2, 0, 0, 0, 0, 0, 3, 3, 1, 0,
0, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 0,
0, 1, 1, 0, 1, 1, 2, 2, 2, 2, 2, 0, 1, 0,
0, 1, 0, 0, 0, 1, 0, 2, 2, 2, 2, 2, 1, 0,
0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}
cameraA = new Rectangle(0, 0, 5, 5); cameraB = new Rectangle(1, 1, 5, 5); herosA = new Tile(cameraA.X + 2, cameraA.Y + 2, 4); herosA.Position = new Vector2(64, 64); herosB = new Tile(cameraB.X + 2, cameraB.Y + 2, 4); herosB.Position = new Vector2(64, 64); viewportA.Width = graphics.PreferredBackBufferWidth; viewportA.Height = graphics.PreferredBackBufferHeight / 2; viewportB.Y = graphics.PreferredBackBufferHeight / 2; viewportB.Width = graphics.PreferredBackBufferWidth; viewportB.Height = graphics.PreferredBackBufferHeight / 2; base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); foreach (Tile tile in map.TileList) { tile.LoadContent(Content, "tile"); } herosA.LoadContent(Content, "tile"); herosB.LoadContent(Content, "tile"); } protected override void Update(GameTime gameTime) { if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Up)) { if (map.TileList[herosA.Y - 1, herosA.X].Type >= 0) { cameraA.Y -= 1;
239
=Labat FM.book Page 240 Vendredi, 19. juin 2009 4:01 16
240
Développement XNA pour la XBox et le PC
herosA.Y -= 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Right)) { if (map.TileList[herosA.Y, herosA.X + 1].Type >= 0) { cameraA.X += 1; herosA.X += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Down)) { if (map.TileList[herosA.Y + 1, herosA.X].Type >= 0) { cameraA.Y += 1; herosA.Y += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Left)) { if (map.TileList[herosA.Y, herosA.X - 1].Type >= 0) { cameraA.X -= 1; herosA.X -= 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Z)) { if (map.TileList[herosB.Y - 1, herosB.X].Type >= 0) { cameraB.Y -= 1; herosB.Y -= 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.D)) { if (map.TileList[herosB.Y, herosB.X + 1].Type >= 0) { cameraB.X += 1; herosB.X += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.S)) { if (map.TileList[herosB.Y + 1, herosB.X].Type >= 0) { cameraB.Y += 1; herosB.Y += 1; } } if (ServiceHelper.Get().KeyHasBeenPressed(Keys.Q)) {
=Labat FM.book Page 241 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
241
if (map.TileList[herosB.Y, herosB.X - 1].Type >= 0) { cameraB.X -= 1; herosB.X -= 1; } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); GraphicsDevice.Viewport = viewportA; spriteBatch.Begin(); map.Draw(spriteBatch, cameraA); herosA.Draw(spriteBatch); spriteBatch.End(); GraphicsDevice.Viewport = viewportB; spriteBatch.Begin(); map.Draw(spriteBatch, cameraB); herosB.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } Avancé Rien ne vous empêche d’utiliser le même objet SpriteBatch pour dessiner dans différents Viewport. Cependant, vous devrez faire un appel à Begin() et à End() pour chacun des objets Viewport !
Vous pouvez à présent essayer le jeu. Les deux joueurs ont chacun une portion d’écran qui leur est réservée : ils peuvent se déplacer sans problèmes, mais sans se voir (figure 11-3). Figure 11-3
Les deux joueurs ne se voient pas
=Labat FM.book Page 242 Vendredi, 19. juin 2009 4:01 16
242
Développement XNA pour la XBox et le PC
Il vous faut donc créer deux nouveaux objets Tile qui correspondront aux représentations d’un joueur sur la vue de l’autre et inversement. Pour commencer, créez un nouveau type de Tile pour que les deux joueurs n’apparaissent pas de la même couleur sur la même vue. Modifiez simplement le constructeur de la classe et ajoutez une nouvelle branche au switch. public Tile(int y, int x, byte type) : base(new Vector2(x * 32, y * 32)) { this.x = x; this.y = y; switch (type) { case 1: Color = Color.Gray; this.type = TileType.Wall; break; case 3: Color = Color.LightGreen; this.type = TileType.Tree; break; case 2: Color = Color.Blue; this.type = TileType.Water; break; case 4: Color = Color.Black; this.type = TileType.Human; break; case 5: Color = Color.Red; this.type = TileType.Human; break; default: this.type = TileType.Normal; break; } }
Dans une vue, le personnage que le joueur ne contrôle pas apparaîtra donc en rouge. Lors du dessin de la vue du joueur A, vérifiez que le joueur B est dans le champ de la caméra A. Si c’est le cas, modifiez la position à l’écran de sa représentation et dessinez-la. Faites le même traitement pour le joueur A sur la vue du joueur B. Les ajouts à effectuer dans la classe principale Tile herosBOnA; Tile herosAOnB;
=Labat FM.book Page 243 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
protected override void Initialize() { // … herosBOnA = new Tile(0, 0, 5); herosAOnB = new Tile(0, 0, 5); } protected override void LoadContent() { // … herosAOnB.LoadContent(Content, "tile"); herosBOnA.LoadContent(Content, "tile"); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); GraphicsDevice.Viewport = viewportA; spriteBatch.Begin(); map.Draw(spriteBatch, cameraA); herosA.Draw(spriteBatch); if (cameraA.Contains(herosB.X, herosB.Y)) { herosBOnA.Position = new Vector2((herosB.X - cameraA.X) * 32, (herosB.Y ➥ cameraA.Y) * 32); herosBOnA.Draw(spriteBatch); } spriteBatch.End(); GraphicsDevice.Viewport = viewportB; spriteBatch.Begin(); map.Draw(spriteBatch, cameraB); herosB.Draw(spriteBatch); if (cameraB.Contains(herosA.X, herosA.Y)) { herosAOnB.Position = new Vector2((herosA.X - cameraB.X) * 32, (herosA.Y ➥ cameraB.Y) * 32); herosAOnB.Draw(spriteBatch); } spriteBatch.End(); base.Draw(gameTime); }
243
=Labat FM.book Page 244 Vendredi, 19. juin 2009 4:01 16
244
Développement XNA pour la XBox et le PC
À présent, vous pouvez tester le jeu, il fonctionne à merveille (figure 11-4). Figure 11-4
Cette fois-ci, les deux joueurs peuvent se croiser sans problèmes
Personnaliser les différentes vues Maintenant que vous connaissez le B.A-BA des vues avec XNA, voyons quelques précisions sur leur fonctionnement. Nous laisserons l’exemple de jeu précédent de côté. Pour nettoyer le contenu d’une vue et lui appliquer une couleur, sélectionnez la vue concernée puis, comme vous le faites d’ordinaire pour la totalité de l’écran, appelez la méthode Clear() de l’objet GraphicsDevice et passez-lui une couleur. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.Viewport = viewA; GraphicsDevice.Clear(Color.Yellow); base.Draw(gameTime); }
Vos vues ne doivent pas forcément remplir toute la surface de l’écran. Vous pouvez les placer où vous le voulez et leur donner des dimensions farfelues si vous le désirez (figure 11-5). Différentes tailles de vues public class Chapitre11_3 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Viewport viewA; Viewport viewB;
=Labat FM.book Page 245 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
Figure 11-5
Les vues se placent où vous le voulez
public Chapitre11_3() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { viewA.Width = 100; viewA.Height = 100; viewA.X = 50; viewB.Width = 300; viewB.Height = 100; viewB.X = 250; viewB.Y = 100; base.Initialize(); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.Viewport = viewA; GraphicsDevice.Clear(Color.Yellow); GraphicsDevice.Viewport = viewB; GraphicsDevice.Clear(Color.Green); base.Draw(gameTime); } }
245
=Labat FM.book Page 246 Vendredi, 19. juin 2009 4:01 16
246
Développement XNA pour la XBox et le PC
Même si vous utilisez une vue, rien ne vous empêche de dessiner d’une manière plus classique sur la totalité de la fenêtre (figure 11-6). public class Chapitre11_3 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Viewport view; Sprite sprite; Sprite sprite2; public Chapitre11_3() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { view.Width = 300; view.Height = 100; view.X = 250; view.Y = 100; sprite = new Sprite(Vector2.Zero); sprite2 = new Sprite(Vector2.Zero); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); sprite.LoadContent(Content, "GameThumbnail"); sprite2.LoadContent(Content, "GameThumbnail"); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); sprite2.Draw(spriteBatch); spriteBatch.End(); GraphicsDevice.Viewport = view; GraphicsDevice.Clear(Color.Green); spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } }
=Labat FM.book Page 247 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
247
Figure 11-6
Affichage classique dans la fenêtre tout en utilisant une vue
Toutefois, faites attention à l’ordre des dessins. Si vous voulez dessiner dans la fenêtre principale après avoir utilisé une vue particulière, le code suivant ne fonctionnera pas (figure 11-7). Figure 11-7
De cette manière, vous ne pouvez pas redessiner dans la vue principale
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.Viewport = view; GraphicsDevice.Clear(Color.Green); spriteBatch.Begin(); sprite.Draw(spriteBatch);
=Labat FM.book Page 248 Vendredi, 19. juin 2009 4:01 16
248
Développement XNA pour la XBox et le PC
spriteBatch.End(); spriteBatch.Begin(); sprite2.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Vous devrez enregistrer la vue principale pour pouvoir la réutiliser plus loin. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); Viewport mainView = GraphicsDevice.Viewport; GraphicsDevice.Viewport = view; GraphicsDevice.Clear(Color.Green); spriteBatch.Begin(); sprite.Draw(spriteBatch); spriteBatch.End(); GraphicsDevice.Viewport = mainView; spriteBatch.Begin(); sprite2.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Si vous désirez faire jouer plus de quatre joueurs dans la même partie ou si vous désirez connecter des joueurs à travers le réseau, vous ne pourrez pas vous contenter du jeu en écran séparé.
Le multijoueur en réseau Dans cette deuxième partie, nous allons nous occuper des mécanismes qui vous permettront de réaliser un jeu multijoueur en réseau. Le but est ici de connecter de nombreux joueurs situés à des emplacements géographiques différents. Cependant, vous devrez faire face à de nouvelles contraintes, telles que la gestion des sessions ou encore l’envoi d’informations via le réseau.
S’appuyer sur la plate-forme Live Windows Live est le nom de la plate-forme de services Internet de Microsoft. Le point fort de ces services est qu’ils utilisent tous un système d’authentification unifié : les comptes Live.
=Labat FM.book Page 249 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
249
La gestion du réseau avec XNA est basée sur ce système de comptes Live. On distingue deux types de comptes : • Les comptes Live, enregistrés sur les serveurs de Microsoft et qui sont liés à un gamer tag. Ce type de compte est nécessaire pour créer un jeu multijoueur et pour pouvoir y jouer. • Les comptes locaux, qui, comme leur nom l’indique, ne peuvent être utilisés que localement et qui n’offrent qu’un accès restreint aux services de la plate-forme Live. Pour utiliser le réseau avec XNA, vous passerez par les gamer services. Pour mémoire, ils s’initialisent de la manière suivante : Components.Add(new GamerServicesComponent(this));
En phase de test Lorsque vous testerez les programmes qui utilisent le réseau avec XNA, vous devrez utiliser plusieurs machines (PC, Xbox ou Zune). En effet, il est impossible de lancer plusieurs instances d’une application qui utilise les gamer services sur la même machine.
Comme nous avons déjà vu au chapitre 8 comment ouvrir les différents menus de ces services, il n’en sera donc pas question ici.
Implémenter les fonctionnalités de jeu en réseau Une partie en réseau, ou session, se gère via un objet de type NetworkSession. Les sessions et la connexion à la plate-forme de jeu en réseau
La première méthode de cette classe qu’il vous faut utiliser est la méthode Create(). C’est elle qui permet de créer une partie en réseau. Le tableau 11-1 répertorie les paramètres les plus courants pour cette méthode : Tableau 11-1 Paramètres du premier constructeur de Create() Paramètre
Description
NetworkSessionType sessionType
Type de la session à créer (réseau local, inter ne à la machine, classement sur Internet, etc.).
Int maxLocalGamers
Nombre maximal de comptes locaux autorisés dans la partie.
Int maxGamers
Nombre de joueurs maximal.
Une fois la session créée, appelez sa méthode Update() à chaque frame. Cette méthode réalise les actions suivantes : • envoyer les paquets réseaux ; • mettre à jour l’état de la session ;
=Labat FM.book Page 250 Vendredi, 19. juin 2009 4:01 16
250
Développement XNA pour la XBox et le PC
• récupérer les paquets réseau entrants. L’appel de cette méthode vous épargne les soucis de threads et de synchronisation que vous auriez pu rencontrer en programmant vous-même la gestion du réseau. Dans l’exemple suivant, nous allons créer une partie sur le réseau local lorsque le joueur appuiera sur la touche C de son clavier. Notez que vous pouvez récupérer des informations sur la partie grâce aux propriétés de l’objet NetworkSession. Dans le code ci-dessous, une vérification de la propriété IsHost permet de déterminer si l’instance du programme héberge la partie ou non. Exemple de création d’une partie sur le réseau local public class Chapitre11_Server : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; NetworkSession session; public Chapitre11_Server() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ServiceHelper.Game = this; Components.Add(new KeyboardService(this)); Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void Update(GameTime gameTime) { if (ServiceHelper.Get().KeyHasBeenPressed(Keys.C) && ➥ session == null) session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31); if (session != null) { if (session.IsHost) Window.Title = "Je suis le serveur (" + session.SessionState ➥ .ToString() + ")"; session.Update(); } base.Update(gameTime); }
=Labat FM.book Page 251 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
251
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Identifier les parties en cours disponibles
Pour rejoindre une partie, vous devez d’abord rechercher une liste de parties disponibles. Pour cela, vous disposez de la méthode statique Find() de la classe NetworkSession, ou bien, si vous souhaitez utiliser des processus asynchrones, des méthodes BeginFind() et EndFind(). Comme nous l’avons vu au chapitre 8, la méthode synchrone bloque le jeu, alors que la méthode asynchrone effectue le traitement en arrière-plan. Là encore, vous devez spécifier le type de partie réseau. Vous pouvez ajouter un paramètre NetworkSession Properties, qui filtre la liste de parties disponibles. Dans les deux cas, vous récupérerez une collection de AvailableNetworkSession de type AvailableNetworkSessionCollection.
Pour rejoindre une partie, vous n’avez plus qu’à utiliser la méthode Join de la classe NetworkSession.
Dans l’exemple suivant, si le joueur appuie sur la touche C (ou le bouton A de la manette Xbox), un serveur est créé. S’il appuie sur S (ou B), le programme se recherche les parties disponibles et, si la recherche retourne au moins un résultat, il s’y connecte. Si le programme est client sur un serveur, il affiche le nom du serveur dans sa barre de titre, sinon il affiche « Je suis le serveur » (figure 11-8). Création, recherche de parties et connexion public class Chapitre11_4 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; NetworkSession session; AvailableNetworkSessionCollection availableSessions; public Chapitre11_4() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); }
=Labat FM.book Page 252 Vendredi, 19. juin 2009 4:01 16
252
Développement XNA pour la XBox et le PC
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); } protected override void Update(GameTime gameTime) { if ((Keyboard.GetState().IsKeyDown(Keys.C) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.A)) && session == null) session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31); if ((Keyboard.GetState().IsKeyDown(Keys.S) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.B)) && ➥ SignedInGamer.SignedInGamers.Count != 0 && session == null) { availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink, ➥ 2, null); if (availableSessions != null && availableSessions.Count > 0) session = NetworkSession.Join(availableSessions[0]); } if (session != null) { if (session.IsHost) Window.Title = "Je suis le serveur"; else Window.Title = "Je suis client sur la partie de " + ➥ session.Host.ToString(); session.Update(); } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } }
Figure 11-8
Cette instance du jeu est correctement connectée à une partie en réseau
=Labat FM.book Page 253 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
253
Transmettre les données : la partie est en cours
Pour envoyer des données, vous utilisez un objet de type PacketWriter. Vous y entrez les données à envoyer (un très grand nombre de types de données est supporté), puis passez cet objet à la méthode SendData () du joueur qui envoie les données. Cette méthode attend un autre paramètre correspondant au mode de transmission des données (voir le tableau 11-2). En pratique Tous les joueurs (y compris l’expéditeur) reçoivent ces données.
Tableau 11-2 Méthodes d’expédition Nom
Description
SendDataOptions.Chat
Les données envoyées correspondent à une conversation entre joueurs.
SendDataOptions.InOrder
Les données peuvent être perdues sur le réseau, mais lorsqu’elles arrivent, elles sont toujours dans l’ordre où elles ont été envoyées.
SendDataOptions.None
Les données peuvent être perdues et arriver dans le désordre.
SendDataOptions.Reliable
Les données ne peuvent pas être perdues, mais leur ordre d’arrivée n’est pas garanti.
SendDataOptions.ReliableInOrder
Les données ne peuvent pas être perdues et arriveront dans le bon ordre.
Bien évidemment, si vous utilisez une option qui garantit une bonne intégrité de la transmission (pas de perte et arrivée des données dans le bon ordre), les performances sont altérées. Pour recevoir des données, recourez à un objet de type PacketReader. Lorsque des données viennent d’arriver, la propriété IsDataAvailable du joueur local (objet de type LocalNetworkGamer) est à true. Vous utiliserez ensuite la méthode ReceiveData () du joueur local pour récupérer ces données. Vous devez passer à cette méthode une référence à un objet de type NetworkGamer, qui correspond à l’expéditeur du paquet. Ensuite, pour extraire les données du PacketReader, pensez à la méthode ReadType (), où Type est le type des données à extraire. La méthode Dispose() de l’objet NetworkSession sert à quitter une partie multijoueur. Ensuite, mettez sa référence (et éventuellement celle de la collection AvailableNetwork SessionCollection) à null. L’exemple de code ci-dessous est une amélioration de l’exemple précédent : il vous permet, lorsque vous appuyez sur la touche M du clavier (ou le bouton Y de la manette), d’envoyer l’heure courante sous forme de chaîne de caractères à tous les joueurs (sauf
=Labat FM.book Page 254 Vendredi, 19. juin 2009 4:01 16
254
Développement XNA pour la XBox et le PC
l’expéditeur) connectés au serveur. Lorsque vous appuyez sur Q (ou le bouton Back dans le cas de la manette), il ferme la connexion à la partie. Exemple de dialogue entre client et serveur public class Chapitre11_4 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SpriteFont font; NetworkSession session; AvailableNetworkSessionCollection availableSessions; PacketReader packetReader; PacketWriter packetWriter; string lastStringReceived = ""; public Chapitre11_4() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; Components.Add(new GamerServicesComponent(this)); } protected override void Initialize() { base.Initialize(); packetReader = new PacketReader(); packetWriter = new PacketWriter(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); font = Content.Load("font"); } protected override void Update(GameTime gameTime) { if ((Keyboard.GetState().IsKeyDown(Keys.Q) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.Back)) && session != null) { session.Dispose(); session = null; availableSessions = null; } if ((Keyboard.GetState().IsKeyDown(Keys.C) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.A)) && session == null) session = NetworkSession.Create(NetworkSessionType.SystemLink, 2, 31);
=Labat FM.book Page 255 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
if ((Keyboard.GetState().IsKeyDown(Keys.S) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.B)) && ➥ SignedInGamer.SignedInGamers.Count != 0 && session == null) { availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink, ➥ 2, null); if (availableSessions != null && availableSessions.Count > 0) session = NetworkSession.Join(availableSessions[0]); } if ((Keyboard.GetState().IsKeyDown(Keys.M) || GamePad.GetState ➥ (PlayerIndex.One).IsButtonDown(Buttons.Y)) && session != null) { packetWriter.Write(DateTime.Now.ToString()); session.LocalGamers[0].SendData(packetWriter, SendDataOptions.); } if (session != null) { session.Update(); LocalNetworkGamer gamer = session.LocalGamers[0]; if (gamer.IsDataAvailable) { NetworkGamer sender; gamer.ReceiveData(packetReader, out sender); if (gamer != sender) lastStringReceived = packetReader.ReadString(); } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); if (session != null) { if (session.IsHost) spriteBatch.DrawString(font, "Serveur (" + SignedInGamer ➥ .SignedInGamers[0].Gamertag + ")", new Vector2(100, 100), ➥ Color.White); else spriteBatch.DrawString(font, "Client (" + SignedInGamer ➥ .SignedInGamers[0].Gamertag + ") connecte sur " + ➥ session.Host.ToString() , new Vector2(100, 100), Color.White);
255
=Labat FM.book Page 256 Vendredi, 19. juin 2009 4:01 16
256
Développement XNA pour la XBox et le PC
spriteBatch.DrawString(font, session.AllGamers.Count + " joueur(s) ➥ connecte(s)", new Vector2(100, 150), Color.White); for (int i = 0; i < session.AllGamers.Count; i++) { spriteBatch.DrawString(font, session.AllGamers[i].Gamertag, new ➥ Vector2(100, 200 + (i * 50)), Color.White); } spriteBatch.DrawString(font, "Dernier message recu", new Vector2(500, ➥ 100), Color.White); spriteBatch.DrawString(font, lastStringReceived, new Vector2(500, 150), ➥ Color.White); session.Update(); } spriteBatch.End(); base.Draw(gameTime); } }
La figure 11-9 présente l’écran du serveur (sur la Xbox 360) et sur la figure 11-10, l’écran du client (sur l’ordinateur). Dans les deux cas, vous retrouvez la liste des joueurs présents dans la session, ainsi que la date du dernier message reçu.
Figure 11-9
L’écran côté serveur (sur Xbox 360)
Figure 11-10
L’écran côté client
=Labat FM.book Page 257 Vendredi, 19. juin 2009 4:01 16
Le mode multijoueur CHAPITRE 11
257
En résumé Dans ce chapitre, vous avez découvert : • comment créer un jeu en deux dimensions avec un effet de scrolling ; • comment gérer des vues avec des objets de type Viewport pour créer un jeu multijoueur sur le même écran ; • ce qu’est la plate-forme Windows Live ; • comment fonctionnent les communications réseau avec XNA. À la fin de ce chapitre, vous disposez de toutes les connaissances et notions nécessaires pour développer un jeu multijoueur en deux dimensions. Voyons à présent comment ajouter la troisième dimension/entrer dans la troisième dimension.
=Labat FM.book Page 258 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 259 Vendredi, 19. juin 2009 4:01 16
12 Les bases de la programmation 3D À ce stade du livre, vous avez normalement déjà écrit plusieurs milliers de lignes de codes, vous avez peut-être même déjà créé plusieurs jeux en 2D, vous les avez peut-être déjà partagés ou vendus, etc. Mais vous ne savez pas encore tout de XNA ! En effet, le framework vous permet non seulement de créer des jeux en 2D, mais il met également la troisième dimension à portée de souris. Ce chapitre constitue une introduction à la programmation en 3D : nous commencerons par découvrir ensemble les bases théoriques, puis vous passerez à la pratique en dessinant vos premières formes, en y ajoutant couleurs, textures et lumières et enfin en chargeant un premier modèle 3D.
L’indispensable théorie Avant de se lancer corps et âme dans la création d’une scène en 3D avec XNA, il est bon de s’intéresser à quelques notions théoriques, faute de quoi vous risqueriez de ne pas vraiment comprendre le code que vous écrivez.
Le système de coordonnées Après avoir lu (et pratiqué !) tous les chapitres précédents, vous connaissez maintenant par cœur le système de coordonnées qui est utilisé par XNA pour les scènes en deux dimensions : l’origine est dans le coin supérieur gauche de l’écran avec les axes représentés tels que sur la figure 12-1.
=Labat FM.book Page 260 Vendredi, 19. juin 2009 4:01 16
260
Développement XNA pour la XBox et le PC
Figure 12-1
Système de coordonnées 2D
Lorsqu’il s’agit de 3D, une nouvelle composante (Z) vient s’ajouter aux deux autres (X et Y). Celle-ci représente la profondeur. Il existe deux types de systèmes de coordonnées pour représenter ces trois axes : le repère main gauche et le repère main droite. La différence entre ces deux repères est la direction de l’axe Z. Essayez : alignez vos mains sur l’axe X, le bout de vos doigts pointant vers la droite, relevez vos doigts (sauf le pouce) dans la direction de l’axe Y et enfin, écartez le pouce du reste des doigts, ce dernier vous donne la direction de l’axe Z. Concrètement (et pour vous éviter de vous casser le poignet), dites-vous que dans le repère main gauche, les valeurs de Z augmentent lorsque vous partez de l’écran vers un point éloigné de vous. Dans le repère main droite, c’est tout simplement l’inverse ; plus les points sont loin de vous, plus les valeurs de Z diminuent (figure 12-2).
Figure 12-2
Repère main gauche et repère main droite
XNA utilise par défaut le repère main droite, donc plus la valeur de Z d’un objet est petite, plus il est loin de vous.
Construire des primitives à partir de vertices L’élément le plus simple qui compose une scène 3D est le vertex (au pluriel, vertices). Le terme vertex est souvent traduit par sommet, or dans XNA, un vertex est bien plus
=Labat FM.book Page 261 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
261
qu’un simple point de l’espace, il contient d’autres informations telles que la couleur, la texture, etc. Lorsque l’on crée des objets en 3D, il faut non seulement spécifier la position des vertices et d’éventuelles informations, mais également préciser le type de primitive à utiliser. Une primitive définit la manière dont une collection de vertices doit être affichée. Au final, on ne dessine donc pas directement les vertices, mais les primitives. Les vertices peuvent être dessinés sous forme de points déconnectés les uns des autres, sous forme de lignes ou encore sous forme de triangles. Généralement, c’est cette dernière catégorie qui est utilisée pour dessiner n’importe quel objet. Ainsi, pour dessiner un carré, vous aurez besoin de deux triangles (figure 12-3). Figure 12-3
Un carré composé de triangles
Dans XNA, les différents types de primitives sont contenus dans l’énumération PrimitiveType. Le tableau 12-1 répertorie les primitives à votre disposition. Tableau 12-1 Primitives 3D Nom
Description
PointList
Les vertices sont isolés les uns des autres.
Illustration Figure 12-4
Les vertices isolés les uns des autres
LineList
LineStrip
Les vertices sont représentés par paire, les éléments de chaque paire étant reliés entre eux. Attention, vous devrez passer un nombre pair de vertices, sans quoi le dessin sera impossible.
Figure 12-5
Tous les vertices sont représentés en une seule ligne. Cette méthode est utilisée dans le mode wire-frame (fil de fer en français), très utile dans les applications en temps réel, puisque facile à afficher.
Figure 12-6
Les vertices groupés par paire
Les vertices composant une seule et même ligne
=Labat FM.book Page 262 Vendredi, 19. juin 2009 4:01 16
262
Développement XNA pour la XBox et le PC
Tableau 12-1 Primitives 3D (suite) Nom
Description
TriangleList
Les vertices sont représentés par groupe de trois, sous la forme de triangles isolés.
Figure 12-7
Les vertices sont représentés sous forme de triangles connectés entre eux. Il y a un gain de performances puisque les vertices communs à deux triangles ne sont représentés qu’une fois.
Figure 12-8
Les vertices sont une nouvelle fois représentés sous forme de triangles, et tous ces triangles ont un vertex en commun.
Figure 12-9
TriangleStrip
TriangleFan
Illustration
Les vertices sous forme de triangles isolés
Les vertices sous forme de triangles connectés entre eux
Les triangles ont tous un vertex en commun
Les vecteurs dans XNA Dans XNA, il existe trois types de vecteurs : • les objets Vector2, que vous avez déjà rencontrés lorsque nous avons travaillé sur des scènes en deux dimensions ; • les objets Vector3, qui disposent en plus de la composante Z ; • les objets Vector4, qui disposent d’une quatrième composante, susceptible d’être utilisée par exemple pour contenir des informations sur la couleur. Une fois de plus, XNA nous simplifie la vie en fournissant des méthodes de calcul sur les vecteurs (notez tout de même que le fait de comprendre les aspects mathématiques qui se cachent derrière ces fonctions ne peut être que bénéfique). Ainsi, la structure Vector3 dispose d’une vingtaine de méthodes utilitaires. Le tableau 12-2 en présente quelques-unes. Lorsque vous développerez vos premiers jeux en 3D, vous utiliserez la plupart de ces méthodes. Tableau 12-2 Principales méthodes de calcul sur les vecteurs Méthode
Description
Add
Effectue la somme de deux vecteurs.
Substract
Soustrait un vecteur à un autre.
Distance
Calcule la distance entre deux vecteurs.
=Labat FM.book Page 263 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
263
Les matrices et les transformations Les matrices constituent l’outil mathématique indispensable pour effectuer : • des rotations, pour faire tourner un objet autour d’un ou plusieurs axes ; • des mises à l’échelle, que cela soit pour agrandir ou diminuer la taille d’un objet ; • des translations, c’est-à-dire déplacer un objet. Les matrices sont des tableaux de données à deux dimensions. Dans XNA, ce tableau contient 4 lignes et 4 colonnes, et il est représenté par la structure Matrix. Comme toujours, XNA nous simplifie le travail. Même si vous n’avez jamais entendu parler de matrices et que vous ignorez tout des règles de calcul qui s’appliquent, vous ne serez pas gêné dans leur manipulation.
Conseil Toutefois, pour comprendre parfaitement les transformations, nous vous conseillons de jeter un coup d’œil à un cours d’algèbre linéaire. Citons par exemple :
http://www.librecours.org/cgi-bin/course?callback=info&elt=562).
Tableau 12-3 Principales méthodes de calcul sur les matrices Méthode
Description
CreateRotationX CreateRotationY CreateRotationZ
Crée une matrice de rotation pour chacun des axes.
Translation
Crée une matrice de translation.
CreateLookAt
Crée une matrice utilisée pour positionner la camera en définissant son emplacement et la position vers laquelle elle est tournée.
Gérer les effets sous XNA La gestion des effets (qu’il s’agisse de la lumière ou de techniques de rendus plus complexes) est un point crucial dans la réalisation d’un jeu. Au chapitre suivant, nous verrons comment créer des fichiers dédiés aux effets en HLSL (High Level Shading Langage). Pour l’instant, nous allons nous concentrer sur le maniement de la classe BasicEffect qui mettra à disposition tout le nécessaire pour un premier jeu. Le tableau 12-4 liste les propriétés qui vous serviront le plus.
=Labat FM.book Page 264 Vendredi, 19. juin 2009 4:01 16
264
Développement XNA pour la XBox et le PC
Tableau 12-4 Principales propriétés de la classe BasicEffect Propriété
Effet
LightingEnabled
Si elle est définie à false, la scène possède une source de lumière qui illumine toutes les faces de tous les objets, sinon, la source de lumière sera celle définie par l’effet.
AmbientLightColor
Cette propriété permet de définir la couleur de la lumière ambiante qui illumine de la même manière toutes les faces de tous les objets si la propriété LightEnabled est définie à true.
DirectionalLight0 DirectionalLight1 DirectionalLight2
Ces propriétés définissent des lumières directionnelles qui seront utilisées seulement si la propriété LightingEnabled est définie à true.
FogColor FogStart FogEnd
Ces propriétés font apparaître du brouillard sur les scènes. Spécifiez la couleur du brouillard, ainsi que la distance où il commence et où il se ter mine.
À la manière d’un objet SpriteBatch, il faut dessiner les objets auxquels vous souhaitez appliquer un effet entre les méthodes Begin() et End() de l’objet BasicEffect. effect.Begin(); // Dessin effect.End();
Un effet possède une ou plusieurs techniques. Chacune de ces techniques possède à son tour une ou plusieurs passes. Une passe contient les différents traitements à effectuer. Pour appliquer un effet, vous devrez donc parcourir la liste de passes d’une technique donnée. Cependant, la classe BasicEffect n’a qu’une seule technique et une seule passe. Nous reviendrons sur ces notions en détail au chapitre 13 dans la section « Finaliser un effet, les techniques et les passes ». effect.Begin(); foreach(EffectPass CurrentPass in effect.CurrentTechnique.Passes) { CurrentPass.Begin(); CurrentPass.End(); } effect.End();
Comprendre la projection Comment XNA transforme-t-il une scène en 3D vers une image en 2D que votre écran pourra afficher ? C’est le principe de la projection, le but étant d’obtenir la même image que ce que nos yeux verraient si nous étions dans la scène. XNA supporte deux types de projection : • La projection en perspective est la plus couramment utilisée. Dans ce type de projection, l’ensemble des objets présents dans un volume en forme de pyramide tronquée (défini par un plan proche (near plane), un plan éloigné (far plane) et un angle appelé champ
=Labat FM.book Page 265 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
265
de vision (FOV, pour field of view)) sont projetés vers le sommet de la pyramide (figure 12-10). Ainsi, plus un objet est éloigné de la caméra, plus il semble petit, et inversement. • La projection orthogonale, aussi appelée projection parallèle. Dans ce type de projection, la composante Z est ignorée, c’est-à-dire qu’un objet éloigné de la caméra semblera aussi gros que s’il était tout proche de celle-ci.
Figure 12-10
Projection en perspective
Vous connaissez à présent tous les éléments que nous allons utiliser pour créer et dessiner des scènes en 3D. La deuxième partie de ce chapitre met ces notions en pratique : nous allons dessiner des formes simples puis les texturer, déplacer la caméra et un objet et enfin charger un modèle en 3D.
Dessiner des formes Passons à présent à la pratique. Dans un premier temps, nous verrons comment configurer la matrice de projection et la matrice de vue, puis comment dessiner des formes dans une scène en 3D.
La caméra et la matrice de projection La première chose à faire avant de dessiner des formes est de créer une caméra, faute de quoi vous ne pourriez pas voir la scène. Créez un nouveau projet et ajoutez quelques champs : Matrix projection; Matrix view; Matrix world;
Ensuite, il faut définir la matrice de projection dans la méthode Initialize(). Pour cela, vous pouvez utiliser la méthode statique CreatePerspectiveFieldOfView (). La méthode
=Labat FM.book Page 266 Vendredi, 19. juin 2009 4:01 16
266
Développement XNA pour la XBox et le PC
attend en paramètre les différentes mesures qui constituent la pyramide tronquée de la projection. Comme l’angle du champ de vision doit être passée en radians, utilisez la méthode ToRadians() de la classe MathHelper, ou comme ici, une des constantes prédéfinies. Il faut également définir le rapport (la variable aspectRatio) entre la largeur de l’écran et sa hauteur. Enfin, le dernier paramètre attendu est une référence vers la matrice où le résultat de cette méthode sera stocké. En pratique Il existe une version non statique de cette méthode. L’intérêt d’utiliser la méthode statique se situe dans les performances : le résultat est directement stocké dans la matrice de destination et ne passe pas par une matrice intermédiaire, ce qui procure un gain de mémoire.
float aspectRatio = graphics.GraphicsDevice.Viewport.Width / ➥ graphics.GraphicsDevice.Viewport.Height; Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f, 1000f, ➥ out projection);
La matrice de vue La deuxième étape concerne la matrice de vue. Vous utiliserez cette fois-ci la méthode CreateLookAt () qui attend trois paramètres : • la position de la caméra ; • la direction visée par la caméra ; • la rotation de la caméra. Ces trois paramètres sont de type Vector3. Ainsi, dans l’extrait de code suivant, vous définissez les objets pour placer une caméra qui regarde vers l’origine du monde en 3D sans rotation particulière. Vector3 cameraPosition = new Vector3(0, 0, 3); Vector3 cameraTarget = Vector3.Zero; Vector3 cameraUpVector = Vector3.Up;
Là encore, il existe une version de la méthode statique qui attend comme dernier paramètre la matrice résultat. Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector, out ➥ view);
=Labat FM.book Page 267 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
267
Des vertices à la forme à dessiner Nous allons à maintenant nous intéresser à l’utilisation concrète des vertices, d’abord pour du dessin en deux dimensions, puis pour le dessin d’un cube. Dessiner en 2D
Chaque vertex de la forme à dessiner à l’écran est défini par un objet de type Vertex PositionColor. Vous stockerez tous ces vertices dans un tableau. Commencez par ajouter un champ à la classe. VertexPositionColor[] vertices;
La classe VertexPositionColor prend deux arguments : la position du vertex et sa couleur. La méthode suivante crée un triangle : private VertexPositionColor[] CreateShape() { VertexPositionColor[] vertices = new VertexPositionColor[3]; // Top left corner vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White); // Bottom right corner vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White); // Bottom left corner vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White); return vertices; }
Vous n’avez plus qu’à dessiner le triangle. Commencez par signaler à l’objet Graphics Device le type de vertex que vous allez utiliser.
Ensuite, déclarez un objet de type BasicEffect. Il faut savoir que, sans effet, vous ne pourrez pas afficher de scène 3D à l’écran. Le premier paramètre attendu par le constructeur est GraphicsDevice. Le second paramètre vous permet de partager des ressources entre plusieurs effets. Fixez-le donc à null, puisque vous n’utilisez ici qu’un seul effet. Ensuite, fixez les propriétés de l’objet, telle que la matrice de projection, celle de la vue, ou encore le type de lumière désiré. Enfin, vous parcourrez les différentes passes de l’effet et appelez la méthode DrawUserPrimitives() de GraphicsDevice. Cette méthode attend le type de primitives à dessiner, les vertices à afficher, l’offset du premier vertex à dessiner et enfin, le nombre de primitives à afficher. Vous avez à présent tous les outils en main pour dessiner un premier triangle (figure 12-11). Ci-dessous se trouve le code source complet de ce premier exemple.
=Labat FM.book Page 268 Vendredi, 19. juin 2009 4:01 16
268
Développement XNA pour la XBox et le PC
Figure 12-11
Le dessin d’une première forme
Premier dessin d’une forme public class Chapitre12 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Matrix projection; Matrix view; Vector3 cameraPosition = new Vector3(0, 0, 3); Vector3 cameraTarget = Vector3.Zero; Vector3 cameraUpVector = Vector3.Up; VertexPositionColor[] vertices; public Chapitre12() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { float aspectRatio = graphics.GraphicsDevice.Viewport.Width / ➥ graphics.GraphicsDevice.Viewport.Height;
=Labat FM.book Page 269 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f, ➥ 1000f, out projection); Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref ➥ cameraUpVector, out view); base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); vertices = CreateShape(); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration ➥ (graphics.GraphicsDevice, VertexPositionColor.VertexElements); BasicEffect effect = new BasicEffect(graphics.GraphicsDevice, null); effect.Projection = projection; effect.View = view; effect.LightingEnabled = false; effect.Begin(); foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes) { CurrentPass.Begin(); graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, ➥ vertices, 0, vertices.Length / 3); CurrentPass.End(); } effect.End(); base.Draw(gameTime); } private VertexPositionColor[] CreateShape() { VertexPositionColor[] vertices = new VertexPositionColor[3]; // Top left corner vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White);
269
=Labat FM.book Page 270 Vendredi, 19. juin 2009 4:01 16
270
Développement XNA pour la XBox et le PC
// Bottom right corner vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White); // Bottom left corner vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White); return vertices; } } En pratique La couleur utilisée ici pour effacer l’écran n’est pas le noir. En fait, il faut toujours éviter d’utiliser le noir comme couleur de fond car si vous rencontrez des problèmes d’éclairage sur l’objet, celui-ci se confondra avec le fond noir de l’écran et vous pourriez penser qu’il n’a pas été dessiné.
Vous auriez pu afficher les vertices sous une autre forme de primitive. Par exemple, sous forme de points : graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0, ➥ vertices.Length);
Ou bien sous forme d’une ligne continue : graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0, ➥ vertices.Length - 1);
Figure 12-12
Les vertices sous forme d’une ligne continue
=Labat FM.book Page 271 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
271
Faites attention à l’ordre dans lequel les vertices doivent être dessinés. En effet, avec XNA, la technique appelée backface culling, qui consiste à éliminer les faces d’un objet qui tourne le dos à la caméra, est activée par défaut pour les vertices déclarés dans le sens inverse des aiguilles d’une montre. Ainsi la ligne suivante est implicitement déclarée : graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
Si vous modifiez l’ordre des vertices de manière à les organiser dans le sens inverse des aguilles d’une montre, vous verrez que plus rien ne s’affiche à l’écran. // Top left corner vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White); // Bottom right corner vertices[2] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White); // Bottom left corner vertices[1] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White);
Maintenant, si vous modifiez la propriété CullMode pour le mode qui cachera les faces dont les vertices sont dans l’ordre des aiguilles d’une montre, le triangle apparaîtra correctement à l’écran. graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
Dans certains cas, vous n’aurez pas besoin d’utiliser la technique du backface culling, vous pourrez alors la désactiver avec la valeur None de l’énumération CullMode ; l’ordre des vertices n’aura plus d’importance. graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
Dessiner un cube
Maintenant que vous savez dessiner un triangle, vous pouvez vous consacrer à des formes plus compliquées… et pourquoi pas un cube. La première face
Avant de vous attaquer au cube complet, commencez par l’un de ses faces. Nous avons vu précédemment qu’un rectangle est composé de deux triangles et qu’il est possible de réutiliser les vertices du premier triangle pour dessiner le deuxième (deux vertices sont communs aux deux triangles). Pour éviter de dupliquer ces points, il suffit d’utiliser un index buffer. Ainsi, au lieu de dupliquer les vertices, vous dupliquez leurs index : vous n’aurez que 4 vertices, mais bien 6 index. 1. Commencez par ajouter le nouveau vertex : private VertexPositionColor[] CreateShape() { VertexPositionColor[] vertices = new VertexPositionColor[4];
=Labat FM.book Page 272 Vendredi, 19. juin 2009 4:01 16
272
Développement XNA pour la XBox et le PC
// Top left corner vertices[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.White); // Bottom right corner vertices[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.White); // Bottom left corner vertices[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White); // Top right corner vertices[3] = new VertexPositionColor(new Vector3(1, 1, 0), Color.White); return vertices; }
2. L’index buffer permet de définir l’ordre dans lequel vous souhaitez que les vertices soient dessinés à l’écran. La méthode suivante place donc les index de manière à ce que les deux triangles soient dessinés dans le sens des aiguilles d’une montre pour ne pas avoir de problèmes avec le backface culling. private short[] CreateIndices() { short[] indices = new short[6]; // Bottom triangle indices[0] = 0; indices[1] = 1; indices[2] = 2; // Top triangle indices[3] = 0; indices[4] = 3; indices[5] = 1; return indices; }
3. Ajoutez ensuite un champ à la classe et définissez-le dans la méthode LoadContent(). short[] indices; protected override void LoadContent() { // … indices = CreateIndices(); }
4. Pour dessiner le rectangle, vous n’utiliserez plus la méthode DrawUserPrimitives(), mais la méthode DrawUserIndexedPrimitives(), qui prendra en compte l’index buffer. graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, ➥ vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
=Labat FM.book Page 273 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
273
5. Compilez le programme, le rectangle s’affiche correctement (figure 12-13).
Figure 12-13
Dessin d’un rectangle
Le cube complet
Vous avez maintenant toutes les clés pour dessiner un cube. Celui-ci doit être composé de 8 vertices. Faites bien attention lors de la création de l’index buffer. Pour ne pas faire d’erreur, aidez-vous par exemple d’un croquis sur papier. Le constructeur de la classe suivante attend également en paramètres un objet Vector3 pour placer le cube, ainsi que la taille des arêtes des cubes. Classe de création d’un cube class Cube { VertexPositionColor[] vertices; short[] indices; BasicEffect effect; GraphicsDevice graphicsDevice; Vector3 position; float widthOver2; public Cube(GraphicsDevice graphicsDevice, Vector3 position, float width, Matrix ➥ projection, Matrix view) { this.graphicsDevice = graphicsDevice; this.position = position; this.widthOver2 = width / 2; effect = new BasicEffect(graphicsDevice, null); effect.Projection = projection;
=Labat FM.book Page 274 Vendredi, 19. juin 2009 4:01 16
274
Développement XNA pour la XBox et le PC
effect.View = view; effect.LightingEnabled = false; InitializeVertices(); InitializeIndices(); } public void Draw() { graphicsDevice.VertexDeclaration = new VertexDeclaration(graphicsDevice, ➥ VertexPositionColor.VertexElements); effect.Begin(); foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes) { CurrentPass.Begin(); graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, ➥ vertices, 0, vertices.Length, indices, 0, indices.Length / 3); CurrentPass.End(); } effect.End(); } private void InitializeVertices() { vertices = new VertexPositionColor[8]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); // Front Bottom right corner vertices[1].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); // Front Bottom left corner vertices[2].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); // Front Top right corner vertices[3].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); // Back Top left corner vertices[4].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); // Back Bottom right corner vertices[5].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); // Back Bottom left corner vertices[6].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); // Back Top right corner
=Labat FM.book Page 275 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
vertices[7].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); } private void InitializeIndices() { indices = new short[36]; // Front indices[0] indices[1] indices[2] indices[3] indices[4] indices[5]
= = = = = =
0; 1; 2; 0; 3; 1;
// Right indices[6] = 3; indices[7] = 6; indices[8] = 1; indices[9] = 3; indices[10] = 4; indices[11] = 6; // Top indices[12] indices[13] indices[14] indices[15] indices[16] indices[17]
= = = = = =
7; 3; 0; 7; 4; 3;
// Back indices[18] indices[19] indices[20] indices[21] indices[22] indices[23]
= = = = = =
4; 5; 6; 4; 7; 5;
// Left indices[24] indices[25] indices[26] indices[27] indices[28] indices[29]
= = = = = =
7; 2; 5; 7; 0; 2;
// Bottom indices[30] = 6; indices[31] = 2;
275
=Labat FM.book Page 276 Vendredi, 19. juin 2009 4:01 16
276
Développement XNA pour la XBox et le PC
indices[32] indices[33] indices[34] indices[35]
= = = =
1; 6; 5; 2;
} }
Plusieurs cubes
La classe ci-dessous dessine deux cubes en utilisant la classe précédente. Nous avons modifié la position de la caméra afin de mieux voir les cubes : nous ne la plaçons plus face au point d’origine, mais nous la décalons (figure 12-14).
Figure 12-14
Dessin de deux cubes
Exemple de dessin de plusieurs cubes public class Chapitre12 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Matrix projection; Matrix view; Vector3 cameraPosition = new Vector3(5, 5, 5); Vector3 cameraTarget = Vector3.Zero; Vector3 cameraUpVector = Vector3.Up; Cube cube;
=Labat FM.book Page 277 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
277
Cube cubeB; public Chapitre12() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { float aspectRatio = graphics.GraphicsDevice.Viewport.Width / ➥ graphics.GraphicsDevice.Viewport.Height; Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.001f, ➥ 1000f, out projection); Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref ➥ cameraUpVector, out view); cube = new Cube(graphics.GraphicsDevice, Vector3.Zero, 1, projection, view); cubeB = new Cube(graphics.GraphicsDevice, new Vector3(-4, 0, 0), 3, ➥ projection, view); base.Initialize(); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); cube.Draw(); cubeB.Draw(); base.Draw(gameTime); } }
Déplacer la caméra Maintenant que vous êtes capable de dessiner des formes 3D à l’écran, il faut encore que vous permettiez au joueur de se déplacer dans ce monde. La caméra que nous allons créer dans les lignes suivantes avancera si le joueur appuie sur la touche Haut, ou reculera si le joueur presse la touche fléchée Bas, et tournera sur elle-même (autour de l’axe Y) si le joueur presse les touches Gauche ou Droite. Commencez par ajouter trois champs à la classe principale : un pour l’orientation de la caméra, un pour la vitesse de déplacement de la caméra et un dernier pour la vitesse de rotation de la caméra. float cameraYaw = 0; float moveSpeed = 0.1f; float rotationSpeed = 0.05f;
=Labat FM.book Page 278 Vendredi, 19. juin 2009 4:01 16
278
Développement XNA pour la XBox et le PC
Dans un premier temps, mettez à jour la position de la caméra en fonction des entrées utilisateur : translations sur les axes X et Z ou rotation autour de l’axe Y. Enfin, dernière chose, mettez à jour la position visée par la caméra, ainsi que la matrice de vue. Le code ci-dessous présente l’ensemble de la méthode Update() qui implémente maintenant la nouvelle caméra. Exemple de déplacement de la caméra protected override void Update(GameTime gameTime) { Matrix movement = Matrix.CreateRotationY(cameraYaw); if(Keyboard.GetState().IsKeyDown(Keys.Up) || Keyboard.GetState() ➥ .IsKeyDown(Keys.Down)) { Vector3 vector = Vector3.Zero; if (Keyboard.GetState().IsKeyDown(Keys.Up)) vector = new Vector3(0, 0, moveSpeed); if (Keyboard.GetState().IsKeyDown(Keys.Down)) vector = new Vector3(0, 0, -moveSpeed); vector = Vector3.Transform(vector, movement); cameraPosition.Z += vector.Z; cameraPosition.X += vector.X; } if (Keyboard.GetState().IsKeyDown(Keys.Left)) cameraYaw += rotationSpeed; if (Keyboard.GetState().IsKeyDown(Keys.Right)) cameraYaw -= rotationSpeed; if (Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); cameraTarget = cameraPosition + Vector3.Transform(Vector3.Backward, movement); Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector, ➥ out view); base.Update(gameTime); }
Il ne vous reste plus qu’à tester la nouvelle caméra (figure 12-15). Pour aller plus loin Sur le site de MSDN, vous trouverez plusieurs exemples de création de caméras, en vue subjective, à la troisième personne… Il est également expliqué comment faire suivre une ligne à la caméra :
http://msdn.microsoft.com/en-us/library/bb203904.aspx/
=Labat FM.book Page 279 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
279
Figure 12-15
Parcourez la scène 3D
Appliquer une couleur à un vertex Les cubes blancs ne sont pas des plus esthétiques, vous en conviendrez aisément. Il est temps de les rendre plus joyeux en leur donnant de la couleur. Jusqu’à présent, vous avez utilisé des vertices de type VertexPositionColor qui, comme leur nom l’indique, possèdent deux propriétés : la première concerne la position dans l’espace et la seconde, la couleur. Reprenez la méthode InitializeVertices() (un simple copier-coller suffit) de la classe Cube et pour chaque vertex, modifiez la propriété Color : private void InitializeVertices() { vertices = new VertexPositionColor[8]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[0].Color = Color.White; // Front Bottom right corner vertices[1].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[1].Color = Color.Red; // Front Bottom left corner vertices[2].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2);
=Labat FM.book Page 280 Vendredi, 19. juin 2009 4:01 16
280
Développement XNA pour la XBox et le PC
vertices[2].Color = Color.Green; // Front Top right corner vertices[3].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[3].Color = Color.Yellow; // Back Top left corner vertices[4].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[4].Color = Color.Blue; // Back Bottom right corner vertices[5].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[5].Color = Color.Orange; // Back Bottom left corner vertices[6].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[6].Color = Color.Black; // Back Top right corner vertices[7].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[7].Color = Color.Violet; }
N’oubliez pas d’activer la gestion des couleurs pour les vertices auprès de l’effet : effect.VertexColorEnabled = true;
Le résultat de cette manipulation est visible sur la figure 12-16 : chaque sommet du cube dispose de sa propre couleur et les couleurs se mélangent lorsqu’elles chevauchent celle d’un vertex voisin. Figure 12-16
Un cube coloré
=Labat FM.book Page 281 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
281
Plaquer une texture sur un objet Le but de cette section est de vous apprendre ce que sont les coordonnées de texture, comment appliquer une texture sur une face d’un objet, mais aussi comment l’appliquer sur un objet tout entier.
Texturer une face d’un objet Pour simplifier les choses, vous allez plaquer une première texture, non pas sur un cube, mais sur une de ses faces. Nous allons travailler sur un simple carré. La première chose à comprendre est la notion de coordonnées de texture. Le terme « coordonnée de texture » désigne tout simplement une position au sein de la texture. Cette position doit être comprise entre 0 et 1, le point d’origine étant placé sur le coin supérieur gauche de la texture (figure 12-17). Ainsi, le coin inférieur gauche a pour coordonnées (0 ; 1) et le coin inférieur droit (1 ; 1). Figure 12-17
Repère de coordonnées de texture
Pour plaquer une texture, il faut associer chaque vertex à une paire de coordonnées de texture. Ainsi, au lieu d’utiliser des vertices de type VertexPositionColor, nous allons utiliser ceux de type VertexPositionTexture. Pour chaque vertex, définissez sa position à l’écran via la propriété Position, mais aussi ses coordonnées de texture grâce à la propriété TextureCoordinate. Dans l’exemple suivant, à chaque sommet du carré, vous associez un coin de la texture. private void InitializeVertices() { vertices = new VertexPositionTexture[8]; // Top left corner vertices[0].Position = new Vector3(-1, 1, 0); vertices[0].TextureCoordinate = new Vector2(0, 0); // Bottom right corner vertices[1].Position = new Vector3(1, -1, 0); vertices[1].TextureCoordinate = new Vector2(1, 1);
=Labat FM.book Page 282 Vendredi, 19. juin 2009 4:01 16
282
Développement XNA pour la XBox et le PC
// Bottom left corner vertices[2].Position = new Vector3(-1, -1, 0); vertices[2].TextureCoordinate = new Vector2(0, 1); // Top right corner vertices[3].Position = new Vector3(1, 1, 0); vertices[3].TextureCoordinate = new Vector2(1, 0); }
Chargez la texture comme vous aviez l’habitude de le faire pour un jeu en 2D. La texture que nous utilisons ici est un simple carré blanc où est écrit « XNA ». Texture2D texture; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); texture = Content.Load("texture"); }
N’oubliez pas de modifier l’objet VertexDeclaration pour qu’il prenne bien en compte un objet de type VertexPositionTexture et non plus VertexPositionColor. graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration ➥ (graphics.GraphicsDevice, VertexPositionTexture.VertexElements);
Dernière chose, signalez à l’effet que vous voulez texturer les vertices. Pour ce faire, passez sa propriété TextureEnabled à true et passez à sa propriété Texture la texture à utiliser. effect.TextureEnabled = true; effect.Texture = texture;
Tout est prêt, compilez le programme et admirez le résultat à la figure 12-18.
Figure 12-18
Une première texture sur un objet en 3D
=Labat FM.book Page 283 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
283
Voyons comment affiner le positionnement de la texture. En jouant sur les indices, vous affichez par exemple uniquement le quart supérieur gauche de la texture (figure 12-19). private void InitializeVertices() { vertices = new VertexPositionTexture[8]; // Top left corner vertices[0].Position = new Vector3(-1, 1, 0); vertices[0].TextureCoordinate = new Vector2(0, 0); // Bottom right corner vertices[1].Position = new Vector3(1, -1, 0); vertices[1].TextureCoordinate = new Vector2(0.5f, 0.5f); // Bottom left corner vertices[2].Position = new Vector3(-1, -1, 0); vertices[2].TextureCoordinate = new Vector2(0, 0.5f); // Top right corner vertices[3].Position = new Vector3(1, 1, 0); vertices[3].TextureCoordinate = new Vector2(0.5f, 0); } Figure 12-19
Une texture partiellement affichée
Si les coordonnées de texture que vous passez aux vertices sont supérieures à 1, la texture sera répétée (figure 12-20). private void InitializeVertices() { vertices = new VertexPositionTexture[8]; // Top left corner vertices[0].Position = new Vector3(-1, 1, 0); vertices[0].TextureCoordinate = new Vector2(0, 0);
=Labat FM.book Page 284 Vendredi, 19. juin 2009 4:01 16
284
Développement XNA pour la XBox et le PC
// Bottom right corner vertices[1].Position = new Vector3(1, -1, 0); vertices[1].TextureCoordinate = new Vector2(2, 2); // Bottom left corner vertices[2].Position = new Vector3(-1, -1, 0); vertices[2].TextureCoordinate = new Vector2(0, 2); // Top right corner vertices[3].Position = new Vector3(1, 1, 0); vertices[3].TextureCoordinate = new Vector2(2, 0); }
Figure 12-20
Une texture répétée
Nous avons vu à la section « Appliquer une couleur à un vertex » comment associer une couleur à un vertex. En fait, les possibilités offertes sont plus vastes : vous pouvez associer une texture et une couleur à un vertex ! Il vous faudra alors utiliser des objets de type VertexPositionColorTexture au lieu des objets VertexPositionTexture. private void InitializeVertices() { vertices = new VertexPositionColorTexture[8]; // Top left corner vertices[0].Position = new Vector3(-1, 1, 0); vertices[0].TextureCoordinate = new Vector2(0, 0); vertices[0].Color = Color.Blue; // Bottom right corner vertices[1].Position = new Vector3(1, -1, 0);
=Labat FM.book Page 285 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
285
vertices[1].TextureCoordinate = new Vector2(2, 2); vertices[1].Color = Color.Green; // Bottom left corner vertices[2].Position = new Vector3(-1, -1, 0); vertices[2].TextureCoordinate = new Vector2(0, 2); vertices[2].Color = Color.Red; // Top right corner vertices[3].Position = new Vector3(1, 1, 0); vertices[3].TextureCoordinate = new Vector2(2, 0); vertices[3].Color = Color.Yellow; }
Cette fois encore, n’oubliez pas de modifier le VertexDeclaration, puis de signaler à l’effet que les vertex utiliseront une couleur. graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration ➥ (graphics.GraphicsDevice, VertexPositionColorTexture.VertexElements); effect.VertexColorEnabled = true;
Le résultat est présenté à la figure 12-21. L’utilisation de la classe BasicEffect est d’une facilité déconcertante, n’hésitez pas à y recourir. Figure 12-21
Le carré coloré et texturé
Texturer un objet entier Texturer un cube tout entier n’est pas aussi simple que texturer une seule de ses faces. Reprenez la classe Cube écrite précédemment et utilisez des VertexPositionColorTexture plutôt que des VertexPositionColor (figure 12-22).
=Labat FM.book Page 286 Vendredi, 19. juin 2009 4:01 16
286
Développement XNA pour la XBox et le PC
private void InitializeVertices() { vertices = new VertexPositionColorTexture[8]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[0].Color = Color.White; vertices[0].TextureCoordinate = new Vector2(0, 0); // Front Bottom right corner vertices[1].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[1].Color = Color.Red; vertices[1].TextureCoordinate = new Vector2(2, 2); // Front Bottom left corner vertices[2].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[2].Color = Color.Green; vertices[2].TextureCoordinate = new Vector2(0, 2); // Front Top right corner vertices[3].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[3].Color = Color.Yellow; vertices[3].TextureCoordinate = new Vector2(2, 0); // Back Top left corner vertices[4].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[4].Color = Color.Blue; vertices[4].TextureCoordinate = new Vector2(0, 0); // Back Bottom right corner vertices[5].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[5].Color = Color.Orange; vertices[5].TextureCoordinate = new Vector2(2, 2); // Back Bottom left corner vertices[6].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[6].Color = Color.Black; vertices[6].TextureCoordinate = new Vector2(0, 2); // Back Top right corner vertices[7].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[7].Color = Color.Violet; vertices[7].TextureCoordinate = new Vector2(2, 0); }
=Labat FM.book Page 287 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
287
Figure 12-22
Un problème lors du placage de la texture
Il semble qu’il y ait un problème d’affichage… La raison est simple : vous n’avez que 8 vertices et les coordonnées de texture ont été définies de façon à bien correspondre pour la face avant et la face arrière du cube. La solution est tout aussi simple : déclarez 4 vertices pour chaque face du cube, soit 24 vertices au total, et paramétrez correctement les coordonnées de texture pour chacune des faces : private void InitializeVertices() { vertices = new VertexPositionColorTexture[24]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[0].TextureCoordinate = new Vector2(0, 0); vertices[0].Color = Color.White; // Front Bottom right corner vertices[1].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[1].TextureCoordinate = new Vector2(2, 2); vertices[1].Color = Color.White; // Front Bottom left corner vertices[2].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[2].TextureCoordinate = new Vector2(0, 2); vertices[2].Color = Color.White; // Front Top right corner
=Labat FM.book Page 288 Vendredi, 19. juin 2009 4:01 16
288
Développement XNA pour la XBox et le PC
vertices[3].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[3].TextureCoordinate = new Vector2(2, 0); vertices[3].Color = Color.White; // Back Top left corner vertices[4].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[4].TextureCoordinate = new Vector2(0, 0); vertices[4].Color = Color.Blue; // Back Bottom right corner vertices[5].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[5].TextureCoordinate = new Vector2(2, 2); vertices[5].Color = Color.Blue; // Back Bottom left corner vertices[6].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[6].TextureCoordinate = new Vector2(0, 2); vertices[6].Color = Color.Blue; // … }
Nous aurons donc toujours 36 index. Cependant, ils ne seront plus répartis de la même manière : private void InitializeIndices() { // … indices[24] = 16; indices[25] = 17; indices[26] = 18; indices[27] = 16; indices[28] = 19; indices[29] = 17; indices[30] indices[31] indices[32] indices[33] indices[34] indices[35] }
= = = = = =
20; 21; 22; 20; 23; 21;
=Labat FM.book Page 289 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
289
Le résultat est visible sur la figure 12-23. Le fait qu’il n’y ait pas de texture affichée sur la face du dessus et celle du dessous du cube est volontaire.
Figure 12-23
Un cube correctement texturé
En jouant avec les coordonnées de texture, vous pouvez vous arranger pour faire comme si une texture différente était appliquée à chaque face du cube. Le cube de la figure 12-24 utilise cet effet. private void InitializeVertices() { vertices = new VertexPositionColorTexture[24]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[0].TextureCoordinate = new Vector2(0, 0); vertices[0].Color = Color.White; // Front Bottom right corner vertices[1].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[1].TextureCoordinate = new Vector2(0.5f, 1); vertices[1].Color = Color.White; // Front Bottom left corner vertices[2].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[2].TextureCoordinate = new Vector2(0, 1);
=Labat FM.book Page 290 Vendredi, 19. juin 2009 4:01 16
290
Développement XNA pour la XBox et le PC
vertices[2].Color = Color.White; // Front Top right corner vertices[3].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[3].TextureCoordinate = new Vector2(0.5f, 0); vertices[3].Color = Color.White; // … // Right Top left corner vertices[12].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[12].TextureCoordinate = new Vector2(0.5f, 0); vertices[12].Color = Color.Red; // Right Bottom right corner vertices[13].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z - widthOver2); vertices[13].TextureCoordinate = new Vector2(1, 1); vertices[13].Color = Color.Red; // Right Bottom left corner vertices[14].Position = new Vector3(position.X + widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[14].TextureCoordinate = new Vector2(0.5f, 1); vertices[14].Color = Color.Red; // Right Top right corner vertices[15].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); Figure 12-24
Variation sur les coordonnées de texture
=Labat FM.book Page 291 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
291
vertices[15].TextureCoordinate = new Vector2(1, 0); vertices[15].Color = Color.Red; // … }
Dans l’extrait de code précédent, les coordonnées de texture sont appliquées de telle façon que la texture apparaît à cheval sur deux face du cube. Le résultat est visible sur la figure 12-24.
Déplacer un objet avec les transformations À la section « Déplacer la caméra », nous avons vu comment nous promener dans un monde en 3D en déplaçant la caméra. Nous allons à présent voir une autre méthode pour arriver au même effet, mais en appliquant des transformations aux objets. Prenez l’exemple du cube de la figure 12-24. Pour être en mesure de voir les faces cachées, vous appliquez une rotation au cube autour de l’axe Y. Ajoutez une méthode Update() à la classe. Dans cette méthode, vous définissez la matrice de rotation grâce à la méthode CreateRotationY de la classe Matrix, puis vous appliquez le résultat à la propriété World de l’effet : public void Update(GameTime gameTime) { rotation = * Matrix.CreateRotationY(0.3f * (float)gameTime ➥ .TotalGameTime.TotalSeconds); }
Compilez le projet : le cube tourne à une vitesse modérée autour de l’axe Y. L’exemple de code ci-dessous utilise des rotations autour des axes X, Y et Z. public void Update(GameTime gameTime) { effect.World = Matrix.CreateRotationX(0.3f * ➥ (float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateRotationY(0.3f * (float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateRotationZ(0.3f * (float)gameTime.TotalGameTime.TotalSeconds); }
La figure 12-25 montre le résultat de l’extrait de code précédent. De la même manière, modifiez la taille du cube grâce à la fonction CreateScale(). L’exemple de code suivant fait grandir le cube en fonction du temps (figure 12-26). public void Update(GameTime gameTime) { effect.World = Matrix.CreateScale(0.03f * (float)gameTime ➥ .TotalGameTime.TotalSeconds); }
=Labat FM.book Page 292 Vendredi, 19. juin 2009 4:01 16
292
Développement XNA pour la XBox et le PC
Figure 12-25
Un cube qui tourne autour des axes X, Y et Z
Figure 12-26
Un cube qui grossit avec le temps
=Labat FM.book Page 293 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
293
Enfin, vous pouvez appliquer des translations au cube grâce à la méthode Create Translation (). Cette méthode attend un objet de type Vector3. L’exemple suivant applique des translations au cube en fonction des entrées clavier de l’utilisateur. public void Update(GameTime gameTime) { Vector3 translation = Vector3.Zero; if (Keyboard.GetState().IsKeyDown(Keys.Right)) translation = new Vector3(0.1f, 0, 0); if (Keyboard.GetState().IsKeyDown(Keys.Left)) translation = new Vector3(-0.1f, 0, 0); if (Keyboard.GetState().IsKeyDown(Keys.Up)) translation = new Vector3(0, 0, -0.1f); if (Keyboard.GetState().IsKeyDown(Keys.Down)) translation = new Vector3(0, 0, 0.1f); effect.World *= Matrix.CreateTranslation(translation); }
Bien entendu, vous pouvez combiner les différentes transformations. Par exemple, l’extrait de code suivant combine translations et modification de l’échelle (figure 12-27).
Figure 12-27
Le cube grossit et se déplace
=Labat FM.book Page 294 Vendredi, 19. juin 2009 4:01 16
294
Développement XNA pour la XBox et le PC
public void Update(GameTime gameTime) { Vector3 translation = Vector3.Zero; if (Keyboard.GetState().IsKeyDown(Keys.Right)) translation = new Vector3(0.1f, 0, 0); if (Keyboard.GetState().IsKeyDown(Keys.Left)) translation = new Vector3(-0.1f, 0, 0); if (Keyboard.GetState().IsKeyDown(Keys.Up)) translation = new Vector3(0, 0, -0.1f); if (Keyboard.GetState().IsKeyDown(Keys.Down)) translation = new Vector3(0, 0, 0.1f); effect.World *= Matrix.CreateTranslation(translation); effect.World += Matrix.CreateScale(0.1f * ➥ (float)gameTime.TotalGameTime.TotalSeconds); }
Jouer avec les lumières Jusqu’à présent, nous avons laissé de côté un point important, mais il prend toute sa signification dans le dessin d’une scène en 3D de qualité : la lumière. C’est grâce à elle que vous rendrez une scène plus vivante, en lui donnant une certaine ambiance et en imaginant des mécanismes du gameplay basés sur elle.
Les différents types de lumière La première chose à savoir est qu’il existe plusieurs types de lumière : • La lumière diffuse (diffuse lighting), qui vient d’une direction particulière. L’intensité de ce type de lumière dépend de l’angle entre la surface de l’objet éclairé et la direction de la lumière : plus l’angle se rapproche de 90°, plus la lumière est réfléchie. Après avoir rencontré une surface, elle est renvoyée uniformément dans toutes les directions. • La lumière spéculaire (specular light), qui vient d’une direction particulière et qui repart dans une direction particulière. Ce genre de lumière peut généralement se repérer par un petit point intensément brillant. • La lumière d’ambiance (ambient light), c’est-à-dire le type de lumière qui ne semble pas venir d’une source en particulier, mais qui semble faire partie de l’environnement. Elle permet aux objets de ne pas sembler totalement noirs. Pour utiliser une lumière diffuse ou une lumière spéculaire, vous devez connaître l’angle entre la surface d’un objet et la direction des rayons de lumière. Ce n’est pas la peine de vous saisir de votre rapporteur, l’effet se chargera de ce travail. La seule chose que vous
=Labat FM.book Page 295 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
295
devrez lui spécifier, c’est la direction vers laquelle la surface est orientée, c’est-à-dire la direction perpendiculaire à la surface : sa normale.
Éclairer une scène pas à pas Reprenez la classe Cube et modifiez-la de manière à utiliser la structure VertexPosition NormalTexture pour les vertices. L’extrait de code ci-dessous repose sur la méthode InitializeVertices(). Il présente la définition de la normale d’un seul vertex de chaque face (elle est la même pour tous les vertices d’une face). private void InitializeVertices() { vertices = new VertexPositionNormalTexture[24]; // Front Top left corner vertices[0].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[0].TextureCoordinate = new Vector2(0, 0); vertices[0].Normal = new Vector3(0, 0, 1); // Back Top left corner vertices[4].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[4].TextureCoordinate = new Vector2(0, 0); vertices[4].Normal = new Vector3(0, 0, -1); // Left Top left corner vertices[8].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[8].TextureCoordinate = new Vector2(0, 0); vertices[8].Normal = new Vector3(-1, 0, 0); // Right Top left corner vertices[12].Position = new Vector3(position.X + widthOver2, position.Y + ➥ widthOver2, position.Z + widthOver2); vertices[12].TextureCoordinate = new Vector2(0.5f, 0); vertices[12].Normal = new Vector3(1, 0, 0); // Bottom Top left corner vertices[16].Position = new Vector3(position.X - widthOver2, position.Y ➥ widthOver2, position.Z + widthOver2); vertices[16].Normal = new Vector3(0, -1, 0); // Top Top left corner vertices[20].Position = new Vector3(position.X - widthOver2, position.Y + ➥ widthOver2, position.Z - widthOver2); vertices[20].Normal = new Vector3(0, 1, 0); }
=Labat FM.book Page 296 Vendredi, 19. juin 2009 4:01 16
296
Développement XNA pour la XBox et le PC
Commencez par activer la prise en charge de la lumière auprès de l’effet : effect.LightingEnabled = true;
Le cube a disparu : c’est tout à fait normal puisque vous n’avez pas encore défini de lumières. Commencez par vous occuper de la lumière d’ambiance en définissant la propriété AmbientLightColor de l’effet. Cette propriété est de type Vector3 : la composante X correspond au niveau de rouge, la composante Y au niveau de vert et la composante Z au niveau de bleu. Ces trois composantes doivent être définies par un nombre compris entre 0 et 1. La ligne suivante définit une lumière d’ambiance blanche (figure 12-28). effect.AmbientLightColor = new Vector3(1, 1, 1); Figure 12-28
Une lumière d’ambiance blanche
Le rendu suivant fait clairement apparaître qu’une lumière d’ambiance ne doit pas être trop présente. Préférez un gris plus sombre (même quantité de rouge, de vert et de bleu). Le résultat visible sur la figure 12-29 semble être assez concluant. effect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
La classe BasicEffect vous permet d’ajouter jusqu’à trois lumières directionnelles (qu’elles soient diffuses ou spéculaires) : DirectionalLight0, DirectionalLight1 et DirectionalLight2. Dans un premier temps, vous allez utiliser une lumière diffuse. Commencez par activer la lumière directionnelle grâce à sa propriété Enabled. Donnez-lui ensuite sa direction via un objet de type Vector3 (dans l’exemple suivant, elle est dirigée vers le bas et vers le fond de la scène). Enfin, choisissez la couleur qu’elle doit diffuser (figure 12-30). effect.DirectionalLight0.Enabled = true; effect.DirectionalLight0.Direction = Vector3.Normalize(new Vector3(0, -1, -1)); effect.DirectionalLight0.DiffuseColor = new Vector3(1f, 1f, 1f);
=Labat FM.book Page 297 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
297
Figure 12-29
Une lumière d’ambiance moins prononcée
Figure 12-30
Une lumière diffuse qui éclaire le cube
Maintenant, ajoutez une lumière spéculaire à la scène. Activez la lumière directionnelle DirectionalLight1, donnez-lui une direction (dans l’exemple suivant, vers la gauche et vers le bas) et enfin, renseignez la couleur qui doit être utilisée via la propriété SpecularColor. Dans l’exemple suivant, la lumière spéculaire éclaire en vert (figure 12-31). effect.DirectionalLight1.Enabled = true; effect.DirectionalLight1.Direction = Vector3.Normalize(new Vector3(-1, -1, 0)); effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
=Labat FM.book Page 298 Vendredi, 19. juin 2009 4:01 16
298
Développement XNA pour la XBox et le PC
Figure 12-31
Combinaison de lumière ambiante, lumière diffuse et lumière spéculaire
La classe BasicEffect permet également d’ajouter facilement du brouillard aux scènes. Commencez par l’activer avec la propriété FogEnabled, puis définissez la couleur du brouillard avec la propriété FogColor. Dernière chose, définissez la limite proche et la limite distante du brouillard avec les propriétés FogStart et FogEnd. Tout ce qui sera avant FogStart sera affiché normalement, tout ce qui sera après FogEnd ne sera plus visible. effect.FogEnabled = true; effect.FogColor = new Vector3(0.5f, 0.5f, 0.5f); effect.FogStart = 5; effect.FogEnd = 6;
Dernière chose, pour renforcer l’effet, modifiez la couleur de fond du jeu pour la faire coïncider avec celle du brouillard et faire disparaître une partie du cube (figure 12-32). GraphicsDevice.Clear(new Color(127,127,127)); Figure 12-32
Le cube est maintenant noyé dans le brouillard : la présence des lumières est diminuée
=Labat FM.book Page 299 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
299
Charger un modèle Rassurez-vous, dans un jeu en 3D, vous ne passerez pas votre temps à coder des vertices pour obtenir des triangles et former les objets que vous désirez. Nous allons voir dans la dernière partie de ce chapitre comment charger et afficher à l’écran un modèle en 3D. Les modèles sont des objets constitués de polygones créés dans des modeleurs 3D. Les modèles et objets 3D qui peuvent être chargés par défaut par XNA doivent être au format .x ou .fbx. Pour créer de tels objets, vous pouvez utiliser des modeleurs 3D open source tels que Blender (http://www.blender.org/) ou encore TrueSpace (http://www.caligari.com/) ou, si vous êtes un artiste 3D et que vous possédez une licence d’un modeleur professionnel tel que 3ds max, Cinema 4D, etc., rien ne vous empêche de les utiliser. Assurez-vous juste de posséder un exportateur de fichier .x (c’est généralement le cas). Références O. Saraja. La 3D libre avec Blender, 3e édition, éditions Eyrolles, 2008. J.-P. Couwenbergh. 3ds max 2008, éditions Eyrolles, 2008. C. Blazy et E. Roux. Cinema 4D, éditions Eyrolles, 2006.
Dans l’exemple suivant, nous utilisons un vaisseau du starter kit Space War. Un modèle s’ajoute au projet exactement de la même manière qu’une texture ou une autre ressource. Pour utiliser un modèle, pensez à la classe Model. Vous chargerez ensuite le modèle grâce à la méthode Load() du Content Manager. Model model; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); model = Content.Load("Models\\p1_wedge"); }
Pour rendre les choses un peu plus jolies, faites tourner le modèle. float rotation = 0.0f; protected override void Update(GameTime gameTime) { rotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds * ➥ MathHelper.ToRadians(0.1f); base.Update(gameTime); }
Un modèle pouvant être composé de plusieurs mesh, vous devrez les parcourir, puis les dessiner un par un avec la méthode Draw(). Chaque mesh peut également posséder plusieurs effets, pensez à les parcourir eux aussi : protected override void Draw(GameTime gameTime) {
=Labat FM.book Page 300 Vendredi, 19. juin 2009 4:01 16
300
Développement XNA pour la XBox et le PC
graphics.GraphicsDevice.Clear(Color.CornflowerBlue); foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = Matrix.CreateRotationY(rotation); effect.View = view; effect.Projection = projection; } mesh.Draw(); } base.Draw(gameTime); }
Après avoir compilé le projet, vous constatez que ce modèle s’affiche correctement et qu’il tourne sur lui-même (figure 12-33).
Figure 12-33
Votre premier modèle chargé et dessiné à l’écran
Vous pouvez modifier la caméra et ajouter un fond à la scène pour obtenir un rendu similaire à celui présenté à la figure 12-34.
=Labat FM.book Page 301 Vendredi, 19. juin 2009 4:01 16
Les bases de la programmation 3D CHAPITRE 12
301
Figure 12-34
En avant vers un vrai jeu en 3D
En résumé Vous connaissez à présent les bases nécessaires à la création d’un jeu vidéo en 3D : • les bases théoriques de la 3D (vertices, vecteurs, etc.) ; • comment dessiner des primitives ; • comment déplacer une caméra dans une scène 3D ; • comment appliquer une couleur ou une texture à un vertex ; • comment déplacer les objets ; • les différents types de lumière dans XNA et comment les gérer ; • comment charger un modèle en 3D. Le chapitre suivant est consacré au HLSL et à la réalisation d’effets, ce qui vous permettra d’améliorer encore la qualité graphique de vos jeux.
=Labat FM.book Page 302 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 303 Vendredi, 19. juin 2009 4:01 16
13 Améliorer le rendu avec le High Level Shader Language Certains diront que les effets graphiques sont l’ingrédient indispensable à la réalisation d’un bon jeu, d’autres répliqueront qu’il ne s’agit que de fioritures visuelles qui exigent du matériel graphique de plus en plus puissant. Cependant, impossible de faire l’impasse sur des termes comme glow, blur, ou bloom. En effet, aujourd’hui, toutes les grandes productions en usent, et parfois en abusent. Ce chapitre présente les shaders ainsi que le langage que vous utiliserez pour les programmer, à savoir le HLSL. Vous découvrirez aussi plusieurs exemples de shaders et comment les utiliser dans un projet XNA. La première partie de ce chapitre explique ce que sont les shaders et présente le HLSL. Vous découvrirez ensuite la syntaxe du HLSL.
Les shaders et XNA Un shader est un programme directement exécutable par la carte graphique. Il permet de transférer la gestion des lumières, ombres, textures, etc., au processeur graphique et de soulager de cette charge de travail le processeur de l’ordinateur. Ainsi, à partir du même contenu de base, mais en utilisant divers shaders, vous pouvez obtenir des rendus complètement différents.
=Labat FM.book Page 304 Vendredi, 19. juin 2009 4:01 16
304
Développement XNA pour la XBox et le PC
Vertex shaders et pixel shaders On distingue deux types de shaders : les vertex shaders et les pixels shaders. Les vertex shaders sont exécutés sur tous les vertices d’un objet. Si vous dessinez un simple triangle à l’écran, le vertex shader sera exécuté pour les trois vertices qui composent ce triangle. Mais les vertex shaders peuvent également être exécutés sur des sprites en 2D : en effet, ceux-ci sont constitués de 4 vertices (un dans chaque coin du sprite). Quant aux pixels shaders, ils sont exécutés sur tous les pixels visibles à l’écran de l’objet que vous dessinez, qu’il s’agisse d’un objet en 3D ou d’un sprite en 2D. Chronologiquement, le processus est le suivant : 1. Le GPU (processeur graphique) reçoit des paramètres et des vertices (leur position, couleur, texture, etc., tout dépend du choix que vous avez fait). 2. Le vertex shader est d’abord exécuté. En sortie, vous disposez des vertices modifiés (ou non, tout dépend du shader). 3. Ces données passent ensuite à l’étape de rastérisation. Au cours de cette étape, les données vectorielles sont converties en données composées de pixels pouvant être affichés à l’écran. 4. Le résultat de la rastérisation est ensuite envoyé au pixel shader. Après l’exécution de ce programme, le GPU retourne la couleur qui devra être affichée. Comme pour un processeur classique, le processeur graphique ne comprend que des instructions de bas niveau. Ainsi, pour faciliter le travail des développeurs, des langages de plus haut niveau (c’est-à-dire, plus intelligibles) à la syntaxe très proche des langages tels que le C ont été créés. Du côté d’OpenGL, le langage standardisé est le GLSL. Le fabricant de cartes graphiques NVidia a, quant à lui, créé le langage Cg. Enfin, de son côté, Microsoft a créé le HLSL (High Level Shader Language). C’est ce dernier langage qui est utilisé par l’API DirectX, et donc par XNA. Définition OpenGL est une API multi-plate-forme d’affichage en trois dimensions développée par Silicon Graphics.
Dans XNA, pour afficher quelque chose à l’écran vous devez passer obligatoirement par HLSL. Cependant, pour vous faciliter les choses (c’est le mot d’ordre de XNA après tout !) et vous éviter de devoir vous attaquer directement au HLSL, les développeurs ont créé les classes SpriteBatch et BasicEffect. En arrière-plan, ces classes travaillent directement avec des shaders. Vous pouvez d’ailleurs retrouver le code source de ces shaders à cette adresse : http://creators.xna.com/fr-FR/education/catalog/.
=Labat FM.book Page 305 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
305
Ajouter un fichier d’effet dans XNA Les fichiers d’effets écrits en HLSL sont automatiquement gérés par le Content Manager de XNA. Exactement comme vous ajoutez n’importe quel type de fichier (une police de caractères par exemple), vous pouvez ajouter un fichier d’effet à XNA (figure 13-1).
Figure 13-1
Ajout d’un fichier .fx au projet
Vous avez ensuite toute latitude pour éditer le fichier .fx directement dans Visual Studio, avec un bloc-notes ou bien, si vous désirez des fonctionnalités plus avancées, notamment pour déboguer un effet, via FX Composer de NVidia (figure 13-2).
FX Composer Téléchargez FX Composer à l’adresse suivante : http://developer.nvidia.com/object/fx_composer_home.html/ Puis, installez le programme comme une application classique.
=Labat FM.book Page 306 Vendredi, 19. juin 2009 4:01 16
306
Développement XNA pour la XBox et le PC
Figure 13-2
FX Composer, l’éditeur de shaders de NVidia
Syntaxe du langage HLSL Vous allez maintenant découvrir la syntaxe de base du langage HLSL. Celle-ci étant très proche des langages C/C++/C#, vous l’assimilerez très vite.
Les variables HLSL Comme la plupart des langages de programmation, le HLSL permet de déclarer des variables de différents types. Vous pouvez ainsi utiliser des variables de type int, float, bool, etc., et même définir vos propres types grâce aux structures. Définir la matrice
Pour définir les matrices, deux manières différentes s’offrent à vous en HLSL :
=Labat FM.book Page 307 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
307
• La première manière consiste à utiliser le mot-clé floatLxC, où L est le nombre de lignes et C, le nombre de colonnes. La ligne de code suivante sert donc à déclarer une matrice de 4 colonnes par 4 lignes. float4x4 myMatrix;
• La seconde repose sur le mot-clé matrix auquel vous ajoutez le type des données et les dimensions de la matrice : matrix myMatrix;
Accéder au contenu de la matrice
Voici les deux manières d’accéder au contenu de la matrice : • La première consiste à considérer la matrice comme un tableau de tableaux : float element = myMatrix[0][0];
• La deuxième repose sur l’utilisation de la notation pointée « . », comme vous le faites en C# pour accéder aux membres d’une structure ou d’une classe : float element = myMatrix._m23;
À la différence du C#, il n’existe pas de types Vector2, Vector3, etc. Vous utiliserez donc les types vector ou floatX, où X est le nombre de composantes utilisées. Ainsi, vous stockerez la position d’un vertex dans une variable de type vector ou float3. Accéder aux composantes d’un vecteur
L’accès aux différentes composantes d’un vecteur se fait de deux manières : • La première consiste à utiliser les crochets [], comme vous le feriez pour un tableau en C#. float value = myVector[0];
• La deuxième consiste à utiliser la notation pointée « . ». Les composantes peuvent être identifiées par deux noms différents. La première série de noms est rgba et la seconde, xyzw. Par exemple, si vous avez un vecteur color, vous pouvez accéder à son niveau de vert des deux manières suivantes : float green = color.g; float green = color.y;
Le swizzling sert à accéder à plusieurs composantes d’un vecteur en même temps. Dans les deux exemples suivants (qui utilisent les deux méthodes d’accès aux composantes), vous accédez aux composantes rouge et verte d’une couleur : float2 redGreen = { color[0],color[1]}; float2 redGreen = color.rg;
=Labat FM.book Page 308 Vendredi, 19. juin 2009 4:01 16
308
Développement XNA pour la XBox et le PC
Attention à l’erreur ! L’exemple de code suivant est invalide : vous ne pouvez pas mélanger les noms des deux groupes de noms de composantes.
float2 redGreen = color.xg;
Les structures de contrôle Les structures de contrôle vous permettont d’effectuer des traitements avancés pour aboutir à des effets encore plus réussis. Vous retrouvez en HLSL une grande partie des structures de contrôle que vous connaissiez déjà en C# et qui s’utilisent de la même manière, à savoir : • If (si condition vérifiée, alors) ; • While (tant que condition vérifiée, faire) ; • Do (faire tant que condition vérifiée) ; • For (pour X de I a J, faire).
Les fonctions fournies pas le langage Le langage HLSL fournit une très longue liste de fonctions faciles à utiliser dans les shaders. Le tableau 13-1 en cite quelques-unes. La liste complète se trouve dans la documentation de DirectX, à laquelle vous accéderez via la bibliothèque MSDN (voir annexe B). Tableau 13-1 Exemples de fonctions fournies pas le langage Fonction
Description
Cos(x)
Cosinus de x.
Sin(x)
Sinus de x.
Mul(a,b)
Multiplication de la matrice a avec la matrice b.
Pow(x,y)
Retourne x à la puissance y.
Sémantiques et structures pour formats d’entrée et de sortie Les sémantiques servent à lier les entrées et sorties d’une fonction. Par exemple, elles servent à lier la sortie de l’application XNA à l’entrée du vertex shader. Dans le processus de rastérisation, d’autres sémantiques lient la sortie du vertex shader avec l’entrée. Pour finir cette courte présentation, signalons également que le pixel shader reçoit et renvoie lui aussi des sémantiques. Vous déclarez vos propres formats d’entrée et de sortie en utilisant les structures. L’exemple ci-dessous définit le format d’entrée pour un vertex shader.
=Labat FM.book Page 309 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
309
struct VertexInput { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; };
Dans la structure précédente, vous constatez que deux sémantiques sont utilisées : une pour la position du vertex et l’autre pour les coordonnées de texture qui lui sont associées. La liste complète des sémantiques disponibles se trouve dans la documentation de DirectX, le tableau 13-2 présente un court extrait de cette liste. Tableau 13-2 Sémantiques d’entrée pour vertex shader Sémantique
Description
COLOR
Couleur diffuse ou spéculaire.
NORMAL
Normale au vertex.
POSITION
Position du vertex.
TEXCOORD
Coordonnées de texture associées au vertex.
Procédez de la même manière pour les données en sortie du vertex shader (VertexOutput), les données en entrée du pixel shader (PixelInput) et celles en sortie du pixel shader (PixelOutput). Attention, la liste des sémantiques n’est pas la même pour chacune de ces étapes. Incompatibilité de versions Certaines sémantiques ne sont pas toujours valables ou ne s’utilisent pas toujours de la même façon selon la version des vertex shaders ou pixel shaders. Vous devrez donc faire attention à la version des shaders que vous utiliserez lors de la compilation.
Écrire un vertex shader Vous savez à présent tout ce qu’il faut savoir pour écrire un premier vertex shader. Il s’agit d’une simple fonction qui doit retourner un objet de type VertexOutput, qui est une structure que vous avez normalement déclarée. VertexOutput vertexShader(VertexInput input) { VertexOutput output; WorldViewProjection = mul(mul(World, View), Projection); output.Position = mul(Pos, WorldViewProjection); output.TexCoord = input.TexCoord; return(output); }
=Labat FM.book Page 310 Vendredi, 19. juin 2009 4:01 16
310
Développement XNA pour la XBox et le PC
Ce premier vertex shader multiplie les différentes matrices (World, View et Projection) pour transformer la position du vertex, puis recopier les coordonnées de textures.
Écrire un pixel shader Le pixel shader est une fonction qui retourne une couleur, c’est-à-dire un vecteur à 4 dimensions. float4 pixelShader(PixelInput input) : COLOR { return tex2D(TextureSampler, input.TexCoord); }
Le pixel shader ci-dessus retourne la couleur de la texture aux coordonnées de texture de l’objet PixelInput.
Finaliser un effet : les techniques et les passes Comme nous l’avons au chapitre précédent, un fichier d’effets peut contenir plusieurs techniques. Une technique n’est rien de plus qu’un nom et un conteneur de passes. Une passe définit quel vertex shader et quel pixel shader doivent être utilisés. C’est aussi ici que vous choisissez la version des shaders (dans le cas présent, version 1 pour les deux). technique Default { pass P0 { VertexShader = compile vs_1_1 vertexShader(); PixelShader = compile ps_1_1 pixelShader(); } }
Vous savez à présent ce qui se cache derrière les termes de vertex shader ou pixel shader. Vous connaissez également la syntaxe du langage HLSL. Bref, vous avez toutes les clés en main pour écrire vos premiers fichiers d’effet.
Créer le fichier d’effet En reprenant les différents éléments de base d’un fichier d’effets que nous avons vus dans la première partie de ce chapitre, vous reconstituez le fichier suivant. Ce premier fichier d’effet affiche le modèle à l’écran, ni plus, ni moins. Fichier d’effet basique float4x4 World : WORLD; float4x4 View; float4x4 Projection; float4x4 WorldViewProjection : WORLDVIEWPROJECTION; texture Texture;
=Labat FM.book Page 311 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
sampler TextureSampler = sampler_state { texture = ; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; struct VertexInput { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; }; struct VertexOutput { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; }; VertexOutput vertexShader(VertexInput input) { VertexOutput output; WorldViewProjection = mul(mul(World, View), Projection); output.Position = mul(input.Position, WorldViewProjection); output.TexCoord = input.TexCoord; return( output ); } struct PixelInput { float2 TexCoord : TEXCOORD0; }; float4 pixelShader(PixelInput input) : COLOR { return tex2D(TextureSampler, input.TexCoord); } technique Default { pass P0 { VertexShader = compile vs_1_1 vertexShader(); PixelShader = compile ps_1_1 pixelShader(); } }
311
=Labat FM.book Page 312 Vendredi, 19. juin 2009 4:01 16
312
Développement XNA pour la XBox et le PC
Voyons comment complexifier les choses… Dans un projet XNA, reprenez la classe Cube écrite au chapitre précédent. Modifiez la classe de manière à ne plus utiliser la classe BasicEffect, mais la classe Effect : Effect effect;
Chargez ensuite l’effet grâce au Content Manager, comme si vous chargiez une texture, un modèle, etc. effect = Content.Load("FirstEffect");
Vous accédez aux différentes variables globales de l’effet via la propriété Parameters. Les variables sont ensuite indexées selon leur nom. Renseignez donc les variables World, View, Projection et Texture. effect.Parameters["Projection"].SetValue(projection); effect.Parameters["View"].SetValue(view); effect.Parameters["World"].SetValue(Matrix.Identity); effect.Parameters["Texture"].SetValue(texture); Plusieurs techniques dans le même fichier d’effet Si vous aviez eu plusieurs techniques dans le fichier, vous auriez pu définir la propriété CurrentTechnique grâce à la propriété Techniques, où les techniques sont indexées par leur nom.
effect.CurrentTechnique = effect.Techniques["Default"];
Tout est prêt, vous pouvez compiler le projet et le lancer : le cube s’affiche correctement. Pour compliquer encore un peu les choses, modifions ce premier fichier afin qu’il prenne en charge la lumière ambiante. Commencez par ajouter une variable globale dans le fichier. Cette variable devra contenir la couleur de la lumière d’ambiance. float4 AmbientColor : COLOR0;
Vous n’avez plus qu’à appliquer la lumière d’ambiance dans le pixel shader. float4 pixelShader(PixelInput input) : COLOR { return (tex2D(TextureSampler, input.TexCoord) * AmbientColor); }
Le fichier d’effet est maintenant prêt. Repassez dans le fichier cube.cs et définissez la variable AmbientColor. effect.Parameters["AmbientColor"].SetValue(0.5f);
=Labat FM.book Page 313 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
313
Une dernière petite modification pour finir ce premier fichier d’effet : nous allons afficher le cube en mode fil de fer (figure 13-3). Pour cela, il suffit d’ajouter la ligne suivante dans la passe : FillMode = Wireframe;
Figure 13-3
Le cube en mode fil de fer
De la même manière, vous pouvez désactiver le culling, ce qui fera bien apparaître toutes les faces du cube (figure 13-4). CullMode = none;
Figure 13-4
Le cube en mode fil de fer sans culling
=Labat FM.book Page 314 Vendredi, 19. juin 2009 4:01 16
314
Développement XNA pour la XBox et le PC
Faire onduler les objets Le premier fichier d’effets que vous venez de réaliser n’a rien d’extraordinaire. Vous allez maintenant créer un effet plus intéressant que ceux proposés par la classe BasicEffect. Le but de l’effet suivant est de donner un effet de vague à des vertices : celui-ci oscillera en fonction du temps grâce à la fonction sinus. 1. Commencez par ajouter une nouvelle variable globale au fichier qui servira à stocker le temps. float Timer : TIME;
2. Modifiez ensuite le vertex shader pour qu’il change les coordonnées du vertex, qui est quant à lui passé en paramètre. Pour cela, copiez sa position dans un vecteur temporaire. 3. Modifiez la composante x, le calcul utilise la fonction sinus qui prend en paramètre la composante y et le temps courant. 4. Dernière chose, passez ce jeu de coordonnées mis à jour au vertex de sortie. VertexOutput vertexShader(VertexInput input) { float4 Pos = float4(input.Position.xyz,1); Pos.x += sin(Pos.y + Timer); VertexOutput output; WorldViewProjection = mul(mul(World, View), Projection); output.Position = mul(Pos, WorldViewProjection); output.TexCoord = input.TexCoord; return( output ); }
5. Dans le code C#, à chaque passage dans la méthode Draw(), passez le nombre de secondes écoulées au total. effect.Parameters["Timer"].SetValue((float)gameTime.TotalGameTime.TotalSeconds);
Vous pouvez ensuite essayer le projet et admirer le résultat (figure 13-5). Figure 13-5
Le cube déformé par le nouvel effet
=Labat FM.book Page 315 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
315
La texture en négatif À présent, voyons comment modifier le pixel shader de manière à ce que le cube apparaisse en négatif (figure 13-6). Pour cela, vous devez effectuer une simple inversion des couleurs : soustrayez chacune des composantes de la couleur du pixel à 1. float4 pixelShader(PixelInput input) : COLOR { float4 color = 1 - tex2D(TextureSampler, input.TexCoord); return( color ); }
Figure 13-6
Le cube est maintenant affiché en négatif
Jouer avec la netteté d’une texture Le pixel shader suivant fait varier la netteté de l’image (figure 13-7). Il suffit de modifier la couleur du pixel courant en fonction des pixels voisins. 1. Commencez par déclarer une variable globale dans le fichier d’effets. float SharpAmount;
2. Puis, utilisez le pixel shader suivant. float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D( TextureSampler, input.TexCoord); color += tex2D( TextureSampler, input.TexCoord - 0.0001) * SharpAmount; color -= tex2D( TextureSampler, input.TexCoord + 0.0001) * SharpAmount; return( color ); }
=Labat FM.book Page 316 Vendredi, 19. juin 2009 4:01 16
316
Développement XNA pour la XBox et le PC
Figure 13-7
Modification de la netteté du cube
3. Pour apprécier pleinement l’impact de cet effet sur le cube, ajoutez une variable au projet C#. float sharpAmount = 0;
4. Pour faire varier l’ampleur de l’effet, augmentez la valeur de la variable précédente si le joueur appuie sur la touche PageUp et faites-la diminuer s’il presse la touche PageDown. public void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.PageUp)) { sharpAmount += 1; effect.Parameters["SharpAmount"].SetValue(sharpAmount); } else if (Keyboard.GetState().IsKeyDown(Keys.PageDown)) { sharpAmount -= 1; effect.Parameters["SharpAmount"].SetValue(sharpAmount); } }
Flouter une texture Le flou (blur en anglais) s’obtient presque de la même manière que l’effet précédent : la couleur du pixel est déterminée en fonction de la couleur des pixels voisins (figure 13-8), sauf que cette fois ci vous n’utiliserez pas de seuil de netteté. Pour vous permettre de bien visualiser l’effet, la distance séparant le pixel courant et les pixels voisins sera dynamique. float Distance;
=Labat FM.book Page 317 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
317
float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D( TextureSampler, float2(input.TexCoord.x+Distance, input.TexCoord.y+Distance)); color += tex2D( TextureSampler, float2(input.TexCoord.x-Distance, input.TexCoord.y-Distance)); color += tex2D( TextureSampler, float2(input.TexCoord.x+Distance, input.TexCoord.y-Distance)); color += tex2D( TextureSampler, float2(input.TexCoord.x-Distance, input.TexCoord.y+Distance)); color = color / 4; return( color ); }
Comme pour l’effet précédent, le paramètre dynamique sera modifié si le joueur appuie sur la touche PageUp ou sur la touche PageDown. float distance = 0; public void Update(GameTime gameTime) { if (Keyboard.GetState().IsKeyDown(Keys.PageUp)) { distance += 0.001f; effect.Parameters["Distance"].SetValue(distance); } else if (Keyboard.GetState().IsKeyDown(Keys.PageDown)) { distance -= 0.001f; effect.Parameters["Distance"].SetValue(distance); } }
Vous remarquez que sur la figure suivante, la texture appliquée au cube est floutée. Figure 13-8
Le cube flouté
=Labat FM.book Page 318 Vendredi, 19. juin 2009 4:01 16
318
Développement XNA pour la XBox et le PC
Modifier les couleurs d’une texture Les pixels shaders suivants inversent les couleurs d’une texture. Ici, il renvoie la couleur avec toutes ses composantes dans l’ordre classique. float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D(TextureSampler, input.TexCoord); return (color.rgba); }
Dans le pixel shader suivant, la composante rouge est inversée avec la composante verte. float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D( TextureSampler, input.TexCoord); return( color.grba ); }
Il est également possible d’inverser le rouge et le bleu. float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D( TextureSampler, input.TexCoord); return( color.bgra ); }
Un effet intéressant consiste à transformer la texture pour que ses couleurs varient uniquement entre le noir et le blanc (on parle également de niveaux de gris). Votre premier réflexe est peut-être de faire la moyenne des trois composantes. Cependant, l’œil humain ne percevant pas de la même manière les trois composantes, vous devez leur appliquer des coefficients. La Commission internationale de l’éclairage conseille d’utiliser les coefficients suivants : • 0,3 pour le rouge ; • 0,59 pour le vert ; • 0,11 pour le bleu. • Pour convertir une texture en niveau de gris (comme sur la figure 13-9), vous pouvez donc écrire le pixel shader suivant (la fonction dot fait le produit scalaire de deux vecteurs). float4 pixelShader(PixelInput input) : COLOR { float4 color = tex2D( TextureSampler, input.TexCoord); color.rgb = dot(color.rgb, float3(0.3, 0.59, 0.11)); return( color ); }
Sur la figure suivante, la texture appliquée au cube apparaît en niveaux de gris.
=Labat FM.book Page 319 Vendredi, 19. juin 2009 4:01 16
Améliorer le rendu avec le High Level Shader Language CHAPITRE 13
319
Figure 13-9
Une texture transformée en niveaux de gris
En résumé Vous connaissez à présent tout ce qu’il faut pour intégrer des premiers effets à vos jeux, à savoir : • ce qu’est un shader ; • la syntaxe du langage HLSL ; • comment écrire un vertex shader et un pixel shader et comment les intégrer aux jeux.
=Labat FM.book Page 320 Vendredi, 19. juin 2009 4:01 16
=Labat FM.book Page 321 Vendredi, 19. juin 2009 4:01 16
A Visual C# Express 2008 Si vous débutez en C#, ou si vous développez habituellement sous Linux, vous n’avez peut-être encore jamais eu à utiliser un outil de la suite Visual Studio. Cette annexe présente l’interface de Visual C# Express 2008, son éditeur de texte, ses fonctionnalités pour vous faire gagner du temps lorsque vous développez et son débogueur.
Différencier solution et projet Un projet regroupe un ou plusieurs fichiers et vise la création d’un assemblage (appelé assembly en anglais). Un assemblage peut être un fichier .exe, mais aussi une DLL (pour Dynamic Link Libraries, bibliothèque de liens dynamiques en français). Les DLL (attention, il ne s’agit pas forcément de fichiers portant l’extension .dll) correspondent aux projets de type Bibliothèque de classes (figure A-1), ou plus particulièrement pour XNA, aux projets de type « Windows/Xbox 360/Zune Game Library » (figure A-2). Une DLL ne comporte pas de point d’entrée comme la fonction Main, elle ne contient que du code réutilisable par plusieurs applications. Dans tout cet ouvrage, nous avons travaillé soit avec XNA ou bien en mode console. Nous avons même utilisé le moteur physique FarseerPhysics, un projet créé par un autre développeur et qui générait une DLL. Une solution est automatiquement créée lorsque l’on crée un projet. Elle peut bien entendu contenir plusieurs projets, comme lorsque vous écrivez une DLL parallèlement à un projet exécutable et que la bibliothèque devra être utilisée dans ce projet. Dans ce cas, il est conseillé de choisir l’option Créer le répertoire pour la solution, ce qui isole les différents projets de la solution dans des sous-répertoires, rendant le tout beaucoup plus lisible.
=Labat FM.book Page 322 Vendredi, 19. juin 2009 4:01 16
322
Développement XNA pour la XBox et le PC
Figure A-1
Création d’une DLL .Net
Figure A-2
Création d’une DLL XNA
=Labat FM.book Page 323 Vendredi, 19. juin 2009 4:01 16
Visual C# Express 2008 ANNEXE A
323
Personnaliser l’interface L’interface de Visual C# Express 2008 est composée de plusieurs fenêtres (l’explorateur de solutions, l’explorateur de propriétés, etc.). Toutes ces fenêtres peuvent être remaniées comme vous le désirez : vous pouvez les déplacer, les cacher ou encore en afficher d’autres. Par exemple, amusez-vous à fermer toutes les fenêtres pour avoir un écran totalement vierge. Allez ensuite dans le menu Affichage et sélection Explorateur de solution ou utilisez le raccourci clavier Ctrl + W, S. Cliquez avec le bouton droit sur la barre de titre de la fenêtre qui s’est ouverte (figure A-3) : vous pouvez définir si elle doit être flottante ou rattachable à un bord de la fenêtre (on parle également de fenêtre « ancrable »). Figure A-3
Mode d’affichage de la fenêtre
Pour ancrer une fenêtre à un bord de la fenêtre, maintenez le clic sur sa barre de titre vers la marque du bord désiré, elle s’y accroche automatiquement (figure A-4). Figure A-4
La fonction d’ancrage des fenêtres est très pratique
Vous avez la possibilité de n’afficher les fenêtres que lorsque votre souris passe sur le bord de la fenêtre. Lorsque vous cliquez avec le bouton droit sur la barre de titre de la fenêtre, choisissez l’option Masquer automatiquement. Si vous disposez d’un écran assez grand et si vous aimez cette manière de travailler, vous pouvez afficher deux fichiers (ou plus) en parallèle, aussi bien verticalement qu’horizontalement (figure A-5).
=Labat FM.book Page 324 Vendredi, 19. juin 2009 4:01 16
324
Développement XNA pour la XBox et le PC
Figure A-5
Avoir plusieurs fichiers ouverts en même temps peut-il améliorer la productivité ?
L’éditeur de texte L’éditeur de texte de Visual C# Express 2008 est un outil particulièrement paramétrable. La première chose que fait généralement un développeur après avoir installé l’IDE est de choisir d’afficher le numéro des lignes (option qui n’est malheureusement pas activée par défaut). 1. Cliquez sur le menu Outils puis sur Options : vous accédez à la fenêtre des options de l’éditeur de texte. 2. Dans l’arbre à gauche de la fenêtre, choisissez Éditeur de texte. 3. Sélectionnez C#. 4. Enfin, cochez la case Numéros de ligne (figure A-6). Dans cette fenêtre, vous pouvez également définir la taille et le comportement des tabulations, l’affichage des erreurs ou encore le comportement d’IntelliSense. IntelliSense IntelliSense est le composant de Microsoft qui assure la saisie automatique de code et qui, lorsque vous écrivez un programme, vous propose une liste contextuelle d’éléments disponibles (propriétés, méthodes, etc.).
=Labat FM.book Page 325 Vendredi, 19. juin 2009 4:01 16
Visual C# Express 2008 ANNEXE A
325
Figure A-6
Les options de l’éditeur de texte
Les directives #region et #endregion ne sont pas spécifiques à la suite de Microsoft, mais font partie intégrante du langage C#. Elles masquent une partie du code pour améliorer la lisibilité. #region Test public class Class1 { } #endregion
Si vous copiez l’exemple ci-dessous dans l’éditeur de texte de Visual C# Express 2008, vous verrez apparaîtrez un petit signe – à côté de l’expression #region. Lorsque vous cliquez sur ce signe, le contenu est automatiquement masqué et remplacé par le mot Test. Dans l’éditeur de texte, vous pouvez également choisir de masquer le contenu d’une fonction, d’une classe, d’un espace de noms, etc., sans utiliser les régions (figure A-7).
Figure A-7
L’implémentation de la fonction est cachée
=Labat FM.book Page 326 Vendredi, 19. juin 2009 4:01 16
326
Développement XNA pour la XBox et le PC
Pour terminer avec l’éditeur de texte, lorsque vous ajoutez des instructions dans un fichier, une barre verticale jaune apparaît à côté de ces nouvelles lignes (ou des lignes modifiées). La barre deviendra verte une fois que vous aurez sauvegardé votre fichier. Les barres disparaissent dès que le fichier est fermé (figure A-8).
Figure A-8
Changements de couleur de la barre verticale
Les extraits de code Les extraits de code (appelé code snippet en anglais) servent à insérer rapidement du code préconfiguré. L’extrait de code peut être une structure de base du langage (une boucle for par exemple) ou un extrait de code plus complet que vous aurez défini. Pour insérer un snippet : 1. Cliquez avec le bouton droit. 2. Sélectionnez Insérez un extrait…, ou bien utilisez le raccourci clavier Ctrl + K, X. 3. Choisissez ensuite l’extrait de code qui vous intéresse. Sur la figure A-9, vous voyez l’ajout d’un extrait de code pour les propriétés. Vous pouvez vous déplacer entre les différentes zones en couleur grâce à la touche Tab.
Figure A-9
Ajout d’un code snippet
Avec XNA, vous devez souvent réécrire toute un bloc de lignes concernant les directives using… Pour gagner du temps, vous allez donc créer un code snippet qui évitera d’avoir à réécrire toutes ces lignes. L’extrait de code est représenté par un fichier XML qui doit porter l’extension .snippet. Le schéma du fichier est disponible sur le site de MSDN à cette adresse : http://msdn.microsoft.com/ en-us/library/ms171418.aspx.
=Labat FM.book Page 327 Vendredi, 19. juin 2009 4:01 16
Visual C# Express 2008 ANNEXE A
327
Expansion Directives using pour XNA usingxna Insere automatiquement les directives using pour XNA. ➥ Leonard LABAT < ![CDATA[]]> Le CDATA évite l’exécution d’une partie du fichier XML. Ce qui se situe dans cette balise ne sera donc pas parsé.
Cliquez ensuite sur Outils, puis sur Gestionnaire des extraits de code, et enfin sur le bouton Importer… Allez chercher le fichier .snippet. L’extrait de code est maintenant prêt à être utilisé.
Refactoriser Imaginez la situation suivante : vous vous êtes lancé dans la création d’une application (un jeu, une application Windows, etc.) et vous êtes déjà bien avancé. Seulement, au bout d’un moment, vous vous rendez compte que vous avez fait une erreur de design, ou vous n’êtes pas satisfait d’un résultat : les modifications à apporter au projet risquent donc de vous prendre beaucoup de temps. La refactorisation (de l’anglais refactoring) permet de remanier facilement le code. Les différentes possibilités de refactorisation sont accessibles depuis le menu Refactoriser lorsque vous avez sélectionné un élément du code. Si vous avez lu ce livre en entier, vous avez déjà rencontré une technique de refactorisation : le renommage. En effet, par défaut dans tout projet XNA, la classe de base s’appelle Game1. Si vous avez également créé une variable qui s’appelle Game1Int, et que finalement
=Labat FM.book Page 328 Vendredi, 19. juin 2009 4:01 16
328
Développement XNA pour la XBox et le PC
vous décidez de renommer la classe en MyGame, Visual C# Express 2008 procédera de manière intelligente et ne renommera que la classe et ses utilisations. Il existe plusieurs autres techniques : • extraire la méthode, ce qui vous permettra de générer automatiquement une méthode à partir d’instructions que vous aurez sélectionnées ; • extraire l’interface, ce qui vous permettra de créer une interface à partir du nom d’une fonction publique ; • supprimer les paramètres, ce qui vous permettra de supprimer un argument de la signature d’une fonction, etc.
Déboguer une application Avant de déboguer un projet, vous devez vous assurer que vous le générez en mode Debug. Pour cela, vérifiez que c’est bien Debug qui est sélectionné dans la liste Configuration de solutions (figure A-10).
Figure A-10
Sélection de la configuration à appliquer
Attention Avant de distribuer votre application, recompilez-la en mode Release. L’assemblage ainsi généré est optimisé et ne comprend pas de données relatives au débogage.
Pour lancer une application et utiliser le débogueur, cliquez sur le menu Déboguer, puis sur Démarrer le débogueur, ou utilisez le raccourci clavier F5. Pour stopper le débogage, retournez dans le même menu et choisissez cette fois Arrêter le débogage, ou utilisez le raccourci clavier Maj + F5. Les points d’arrêt (break points en anglais) sont un élément essentiel du débogueur : vous en placez sur chaque instruction où le programme doit suspendre son exécution. Ils sont donc pratiques pour vérifier que le programme passe bien par certains endroits. Pour placer un point d’arrêt sur une ligne, cliquez sur la colonne située à gauche de celle-ci ou utilisez le raccourci F9. Le retrait d’un point d’arrêt se fait de la même manière. Lorsque vous avez ajouté un point d’arrêt sur une ligne, celle-ci s’affiche en rouge dans l’éditeur de texte.
=Labat FM.book Page 329 Vendredi, 19. juin 2009 4:01 16
Visual C# Express 2008 ANNEXE A
329
Placez un point d’arrêt quelque part dans le code (par exemple, dans la méthode Update() si vous travaillez sur un jeu avec XNA) et lancez le débogueur. Dès que le programme atteint le point d’arrêt, la fenêtre de Visual C# Express 2008 passe au premier plan. La ligne où s’est arrêté le programme est maintenant visible en jaune dans l’éditeur de texte (figure A-11).
Figure A-11
L’exécution s’est suspendue sur le point d’arrêt
Vous pouvez afficher le contenu d’une variable simplement en passant dessus le curseur de la souris. Pour les objets plus complexes, faites dérouler la liste des informations grâce au signe + (figure A-12).
Figure A-12
Visualisation de la valeur d’une variable
Vous pouvez aussi voir le contenu des variables via les fenêtres espions. Pour ajouter un espion sur une variable, faites un clic droit sur la variable et choisissez Ajouter un espion. Son contenu est alors visible dans la fenêtre Espion (figure A-13). La fenêtre Variables locales présente automatiquement les variables définies dans le contexte courant.
Figure A-13
Utilisation d’un espion
=Labat FM.book Page 330 Vendredi, 19. juin 2009 4:01 16
330
Développement XNA pour la XBox et le PC
Il ne s’agit ici que d’une présentation rapide du débogueur de Visual C# Express 2008. Il possède bien d’autres fonctionnalités et le framework .Net dispose aussi des classes Debug et Trace qui pourraient vous être utiles…
Raccourcis clavier utiles La meilleure façon d’augmenter votre productivité est de maîtriser les différents raccourcis clavier de Visual Studio. Le tableau A-1 énumère les plus couramment utilisés.
Toujours plus de raccourcis Si vous avez envie d’abandonner complètement votre souris et de devenir un as du clavier, Microsoft a mis à la disposition des développeurs C# un poster qui regroupe tous les raccourcis clavier disponibles dans Visual Studio 2008. http://www.microsoft.com/downloads/details.aspx?familyid=E5F902A8-5BB5-4CC6-907E472809749973&displaylang=en Tableau A-1 Raccourcis clavier les plus couramment utilisés Raccourci
Description
Ctrl + M,O
Masquer une zone.
Ctrl + M,M
Déplier une zone masquée.
Ctrl + E,C
Commenter la zone sélectionnée.
Ctrl + E,U
Décommenter la zone sélectionnée.
TAB
Insérer un snippet.
Shift + Alt + C
Afficher la fenêtre d’ajout de classe.
Ctrl + J
Afficher IntelliSense.
Ctrl + F4
Fermer le fichier ouvert.
Ctrl + Shift + B
Générer le projet.
F5
Lancer le débogage.
=Labat FM.book Page 331 Vendredi, 19. juin 2009 4:01 16
B Les bienfaits de la documentation Dans cette annexe, vous allez apprendre où trouver de la documentation et comment générer la documentation de vos projets.
L’incontournable MSDN La première chose à connaître lorsque vous débutez avec XNA ou, d’une manière plus générale, avec la programmation .Net, c’est l’existence de MSDN (Microsoft Developer Network). MSDN, c’est tout simplement : • Un site web, qui est disponible en français (http://msdn.microsoft.com/fr-fr/) et sur lequel vous retrouvez des actualités, des dossiers ou encore des événements à propos des technologies Microsoft (figure B-1). • Une gigantesque bibliothèque de documentations qui contient des tutoriaux et un guide de référence sur toutes les classes du framework .Net ou XNA (figure B-2). Cette base de connaissances est également disponible en téléchargement lorsque vous installez un outil de la suite Visual Studio. • Des forums de discussion où vous pourrez trouver de l’aide auprès des experts de la communauté. • Des blogs où les auteurs postent régulièrement leurs dernières trouvailles ou études, et ce, pas uniquement sur des technologies Microsoft.
=Labat FM.book Page 332 Vendredi, 19. juin 2009 4:01 16
332
Développement XNA pour la XBox et le PC
Figure B-1
Le site web français de MSDN
Figure B-2
La documentation complète du framework est disponible
=Labat FM.book Page 333 Vendredi, 19. juin 2009 4:01 16
Les bienfaits de la documentation ANNEXE B
333
MSDN propose un abonnement qui permet, entre autres, à ceux qui en bénéficient d’accéder aux tout derniers logiciels de Microsoft en avant-première. Enfin, MSDNAA (MSDN Academic Alliance) est un programme qui permet aux universités et écoles adhérentes de proposer à leurs étudiants l’accès à divers logiciels Microsoft. Si vous êtes étudiant et que votre structure enseignante vous le permet, vous avez par exemple accès à un abonnement étudiant à l’XNA Creator Club pour tester vos jeux sur Xbox 360 (vous ne pourrez cependant pas vendre vos jeux).
Ressources sur le Web La communauté XNA est grande, les blogs et les sites des gourous du framework sont très nombreux et leur contenu est à la hauteur. Dans le tableau B-1, nous avons essayé de dresser une courte liste de ces ressources, à visiter et explorer régulièrement en profondeur. Tableau B-1 Sites et blogs incontournables Site
Langue
Description
http://ziggyware.com/
En anglais
Actuellement, presque deux cents ar ticles publiés. Une véritable mine d’or.
http://blogs.msdn.com/shawnhar/
En anglais
Le blog de Shawn Hargreaves.
http://msmvps.com/blogs/valentin/default.aspx
En français
Le blog de Valentin Billotte, MVP.
http://www.c2i.fr/
En français
Le site web de Richard Clark, MVP.
http://blog.emmanueldeloget.com/
En français
Le blog d’Emmanuel Deloget.
http://www.xnadevelopment.com/
En anglais
Le site web de George Clingerman, MVP.
http://leonard-labat.blogspot.com/
En français
Le blog de l’auteur de ce livre.
MVP Le terme MVP (Microsoft Most Valuable Professional) est un titre décerné chaque année par Microsoft à des professionnels des technologies Microsoft indépendants. Les experts qui reçoivent ce titre sont récompensés pour le partage de leurs connaissances auprès d’autres membres des communautés d’utilisateurs.
Figure B-3
Le logo généralement présent sur le blog ou le site d’un MVP
=Labat FM.book Page 334 Vendredi, 19. juin 2009 4:01 16
334
Développement XNA pour la XBox et le PC
Générer de la documentation Vous vous en doutez sûrement, apprendre à programmer sans support de documentation est mission impossible. De la même manière, il est difficilement imaginable que vous puissiez apprendre à vous servir d’une bibliothèque sans pouvoir vous appuyer sur une source de documentation. Vous aurez donc certainement envie, vous aussi, d’utiliser les fonctionnalités de génération de documentation de la suite Visual Studio afin de distribuer vos projets accompagnés de cette source d’informations. Nous l’avons vu au début du livre, il existe un format de commentaire particulier dédié à la documentation, les trois slashs à la suite : ///
À la suite de ces slashs, vous écrivez une balise XML. Le tableau B-2 répertorie une partie des balises existantes. Tableau B-2 Les balises de documentation Balise
Description
Utilisée pour donner la description complète d’une classe, fonction, propriété ou variable.
Utilisée pour la documentation d’un argument. Vous devez préciser le nom de l’argument.
Argument 1
Utilisée pour documenter la valeur de retour d’une fonction.
Utilisée pour documenter une propriété.
Utilisée pour signaler qu’un mot, utilisé par exemple dans une balise , est un paramètre de la fonction.
Je parle de Argument 1
Utilisée pour documenter du code sur une ligne (la différence avec du texte classique se situe au niveau de l’affichage).
Même chose que , mais supporte plusieurs lignes.
Utilisée pour ajouter des remarques ou commentaires par ticuliers.
Utilisée pour préciser le type d’exception que la fonction est susceptible de lever.
Utilisée pour donner un exemple d’utilisation d’un élément.
Utilisée pour créer un lien vers un autre élément.
Avez-vous vu ma méthode ?
Utilisée pour créer un lien en bas de page de la documentation.
Une fois votre classe correctement documentée, signalez à l’environnement de développement qu’il doit générer un fichier .xml contenant la documentation du projet.
=Labat FM.book Page 335 Vendredi, 19. juin 2009 4:01 16
Les bienfaits de la documentation ANNEXE B
335
1. Cliquez sur le menu Projet, puis sur Propriétés de … 2. Allez ensuite sur l’onglet Générer. 3. Cochez la case Fichier de documentation XML (figure B-4).
Figure B-4
Activation de la génération de documentation
4. Générez ensuite le projet et rendez-vous dans son répertoire de sortie : un fichier .xml a bien été créé. - - ChapitreDix_2 - - The main entry point for the application.
Pour transformer votre documentation en une version plus lisible, essayez par exemple l’utilitaire SandCastle (http://www.codeplex.com/Sandcastle) qui génère des fichiers HTML dans le style MSDN (figure B-5).
=Labat FM.book Page 336 Vendredi, 19. juin 2009 4:01 16
336
Développement XNA pour la XBox et le PC
Figure B-5
Documentation générée avec SandCastle
=Labat FM.book Page 337 Vendredi, 19. juin 2009 4:01 16
Index Numériques 3ds max 299 A A* 186, 187 abonnement 228 accesseur 179 accolade 16 ActiveSongChanged 144 addition 9 affichage 287 fil de fer 313 algorithme A* 187 recherche de chemin 186 test 197, 204 alias, espace de noms 19 alpha 216 ambient light 294 AmbientLightColor 296 animation 108 sprite sheet 108 ApplyForce 224 arrière-plan 86, 105 aspectRatio 266 assemblage 321 assembly 321 Asteroids 207 asynchrone 144 méthode 154 traitement 155 attribut Serializable 178 speed 210 XmlIgnore 179 AudioEngine 136 autocomplétion 32
B backface culling 271, 272 Background 87 balises 146 documentation 334 banque de sons 133 BasicEffect 263, 264, 267, 298, 304 BeginShowKeyboardInput 156 BeginShowMessage 154 bibliothèque 29 multimédia 142 de classes 321 BinaryFormatter 180 Blender 299 bloc catch 150 finally 150 blur 316 Body 224 BodyFactory 224 boîte de dialogue 154 booléen 6, 155 IsTrialMode 160 SimulateTrialMode 160 boucle 55 do 55 for 56, 196 foreach 58, 196 infinie 56 while 55, 196 brouillard 298 buffer 139, 172 vider 175 Buttons 72 ButtonState 71 byte 191
C callback 154 caméra 233, 265, 278 champ de vision 233 déplacer 277 multijoueur 238 Rectangle 233 carte bords 235 graphique 303 case 190, 192 coût 192 cast 180 casting 77 casual (jeu) 35 catch 150 CDATA 327 chaîne de caractères 8 concaténer 24 champ de vision 265 char 7 classe 18, 20 Background 87 BasicEffect 263, 264, 298, 304 Body 224 BodyFactory 224 de test 110 Directory 169 DrawableGameComponent 86 droits d’accès 21 Exception 150 File 172, 174 Game 75 Geom 227 Guide 152, 158 hiérarchie 54 KeyboardState 68 Map 191
=Labat FM.book Page 338 Vendredi, 19. juin 2009 4:01 16
338
Développement XNA pour la XBox et le PC
classe (suite) MathHelper 266 Matrix 291 MediaLibrary 142 MediaPlayer 142 mère 51 NetworkSession 251 Node 192 NodeList 195 partielle 22 Path 171 PathFinding 196 PhysicsSimulator 222 Player 228 ServiceHelper 88 SignedInGamer 158 sprite 37, 45 SpriteBatch 99, 304 statique 19 Tile 190, 201 WaveBank 138 clavier 67, 75 déplacement du sprite 69 récupérer l’état 68 Clear 214, 244 Clingerman, George 105, 108 Close 175 code snippet 326 CodePlex 219 collection 58 Components 87 générique 58 List 194, 211 ServiceHelper 201 collision 200, 213 détection 91 pixel 215 Player 228 rectangle 213 CollisionPerPixels 216 Color 31, 112, 279 Combine 171 commentaire 11 Components 87 compression 144 condition 11, 56 configuration utilisateur 146 const 8 constante 8 constructeur 19, 22, 51 argument 23 Vector2 39
ContainsKey 62 conteneur de Références 165 Content Manager 37, 55, 136 HLSL 305 pipeline 29 Processor 144 ContentImporter 181 continue 56 coordonnées 259, 281 Copy 172 couleur 279 moduler 40 négatif 315 netteté 315 texture 318 Vector4 262 Count 196 Create 172, 249 CreateLookAt 266 CreatePerspectiveFieldOfView 265 CreateRectangleBody 224 CreateRectangleGeom 227 CreateRotationY 291 CreateScale 291 CreateTranslation 293 culling 313 CullMode 271 CurrentTechnique 312 D déboguer 328 délégué 228 Delete 172 démo 160 désérialisation 148 Deserialize 180 détection, collision 91 dictionary 59 dictionnaire 62 diffuse lighting 294 Dijkstra 186 directive, using 68, 326 Directory 19, 169 DirectX 29 Dispose 253 distance de Manhattan 188, 192 division 9 par zéro 149 DLL 321 do, boucle 55
documentation 334 balise 334 données extraire 253 recevoir 253 récupérer 253 dossier, jeu 168 dot 318 Draw 30, 40, 48, 99 surcharge 40 DrawableGameComponent 86, 128 DrawUserIndexedPrimitives 272 droits d’accès 21 E échelle, modifier 291, 293 écran, partage 231 écrire dans un fichier 174, 175 éditeur de cartes 162 de texte 324 effet 263 fichier 310 flou 316 négatif 315 netteté 315 niveau de gris 318 prise en charge de la lumière 296 technique 264 vague 314 else 12 else if 13 encapsulation 21 énumération 68 Buttons 72 ButtonState 71 CullMode 271 Keys 68 PlayerIndex 71, 238 PrimitiveType 261 SpriteEffects 116 TileType 192 espace de noms 19, 169, 178, 179 déclaration 68 Microsoft.Xna.Framework.Input 68 de stockage 145 dossier de l’utilisateur 146 dossier du jeu 145 espion 329
=Labat FM.book Page 339 Vendredi, 19. juin 2009 4:01 16
Index
état, souris 201 événement 228 ActiveSongChanged 144 exception 148, 150 personnaliser 151 exe 321 expédition, méthode 253 extrait de code 326 F factoriser le code 14 far plane 264 FarseerPhysics 207, 219 fenêtre 208 feuille de sprites 109 fichier écrire 174, 175 lire 177 manipuler 172 sérialiser 178 field of view 265 fil de fer 313 File 19, 172, 174 finally 150 Find 251 float 116 floatLxC 307 flou 316 Flush 175 flux 174, 175, 176 FogEnabled 298 fonction 15, 17 BeginShowKeyboardInput 156 CollisionPerPixels 216 définir 17 dot 318 mathématique 224 override 51 ResetPosition 229 SetVibration 72 for, boucle 56, 196 force 224 foreach, boucle 58, 196 format CSV 147 INI 148 mp3 133 wav 133 XML 146 FPS (Frames Per Second) 128 frame 46
framework 29 FX Composer 305 G gâchette analogique 72 Game 75 GamePadState 71 gameplay 80 Gamer 158 gamer services 249 tag 249 Gamer Services 152 GamerCard, afficher 158 GamerServicesNotAvailableExce ption 148 GameThumbnail 61 GameTime 88 Geom 227 gestionnaire d’exceptions 148 de contenu 125, 144 image 55, 60 get 22 GetData 215 GetLength 57 GetPressedKeys 69 GLSL 304 GPU 304 graphics 32, 41, 208 GraphicsDevice 244, 267 gravité 222 GUI 80, 114 wWinForms 81 XNA Simple Gui 81 Guide 152, 158 H Half Life 2 217 Hargreaves, Shawn 128 Havok 217 héritage 51, 52 multiple 50 HLSL (High Level Shader Language) 304 I IAsyncResult 155 if 11 image, gestionnaire 55, 60 Imagine Cup 29
IMouseService 200 incrémentation 10 index 271 index buffer 271, 272 inertie 227 Initialize 30, 41, 42, 210, 211 InitializeVertices 279, 295 insertion dichotomique 194 instancier, constructeur 22 instruction continue 56 intelligence artificielle 185 IntelliSense 32, 324 interface 74 extraire 328 IMouseService 200 règle de nommage 74 IsConnected 71 IsDataAvailable 253 IsTrialMode 160 J jeu de rôle 50 en réseau 248 Mario 218 plates-formes 218 tile-based 37 Join 251 joueur connecté 158 K Keyboard 68 KeyboardState 68 KeyHasBeenPressed 232 Keys 68 Keys.Escape 69 L layerDepth 116 lecteur réseau 146 licence 219 lire un fichier 177 List 211 liste 194 fermée 187 générique 195 ouverte 187, 194 triée 194 vider 214 Live, compte 248 Load 299
339
=Labat FM.book Page 340 Vendredi, 19. juin 2009 4:01 16
340
Développement XNA pour la XBox et le PC
LoadContent 30, 48, 61, 211, 272 lumière 294 d’ambiance 294, 296, 312 diffuse 294, 296 directionnelle 296 spéculaire 294, 297 M Main 15 manette 67, 71, 238 gâchette analogique 72 multijoueur 71 pad directionnel 72 stick analogique 72 vibration 72 Manhattan 192 Map 191 Mario 218 Math 19, 20 MathHelper 266 matrice 263 contenu 307 de rotation 291 de vue 266 définir 306 résultat 266 Matrix 263, 291, 307 MediaLibrary 142 MediaPlayer 142, 144 mémoire 3 mesh 299 méthode ApplyForce 224 asynchrone 154, 155, 228 BeginShowMessage 154 Clear 214, 244 Close 175 Combine 171 Copy 172 Create 172, 249 CreateLookAt 266 CreatePerspectiveFieldOfView 265 CreateRectangleBody 224 CreateRectangleGeom 227 CreateRotationY 291 CreateTranslation 293 Delete 172 Deserialize 180 Dispose 253 Draw 30, 40, 48, 99
DrawUserIndexedPrimitives 272 extraire 328 Flush 175 générique 77 GetData 215 GetLength 57 GetPressedKeys 69 Initialize 30, 211 InitializeVertices 295 Join 251 KeyHasBeenPressed 232 Load 299 LoadContent 30, 48, 61, 211 Next 210 Open 174 Play 144 PlayCue 136 privée, Initialize 210 ReadType 253 ReceiveData 253 ResetPosition 210 SendData 253 ShowGamerCard 158 statique 169 Find 251 synchrone 251 UnloadContent 30 Update 30, 42, 48, 71, 208, 209, 211, 214, 278, 291 ValidCoordinates 191 Write 175 WriteLine 175 Microsoft.XNA.Framework.Gam e.dll 29 mise à l’échelle 263 mode démonstration 160 Release 149 modèle 299 ajouter au projet 299 charger 299 modeleur 3D 299 molette 70, 71 mot-clé override 51 throw 151 moteur graphique 29 physique 207, 217 MouseState 70
MSDN (Microsoft Developer Network) 331 MSDNAA (MSDN Academic Alliance) 333 msElapsed 203 multijoueur 237 multiplication 9 N near plane 264 netteté 315 NetworkSession 249, 251, 253 NetworkSessionProperties 251 Next 210 nextColor 112 niveau 187 de gris 318 Node 192 NodeList 195 nœud 187, 192 coût de passage 189, 200 nombre aléatoire 210 entier 4 réel 6 notation pointée 307 O objet 18 AudioEngine 136 BinaryFormatter 180 ContentManager 62 Cue 137 GamePadState 71 Gamer 158 GameTime 88 graphics 41, 208 Keyboard 68 MouseState 70 random 210 SoundBank 136 SpriteBatch 128 SpriteFont 128 Spritefont 125 StorageContainer 153, 168 StreamWriter 175 String 19 TimeSpan 211 Vector2 222 WaveBank 136 Open 174
=Labat FM.book Page 341 Vendredi, 19. juin 2009 4:01 16
Index
OpenGL 304 opérateur && 216 conditionnel 12 incrémentation 10 logique 14 ternaire 214 override 51 P PacketReader 253 PacketWriter 253 pad directionnel 72 paquet 249 paramètre callback 154 PlayerIndex 158 supprimer 328 partial 22 passe 264, 310 Path 171 Pathfinding 186, 196 performances 63, 253, 266 temps d’exécution 63 utilisation mémoire 63 périphérique 67, 154 clavier 67 manette 67, 71 souris 70, 200, 201 spécialisé 73 batterie 73 guitare 73 tapis de danse 73 perspective, projection 264 Phun 218 PhysicsSimulator 222 physique 217 piste, Cue 136 pixel shader 304, 308, 310 plan éloigné 264 proche 264 PlayCue 136 Player 228 PlayerIndex 71, 158, 238 plein écran 33 point d’arrêt 328 police de caractères 126 Pong 83, 231 Portal 217 Position 281
preset 140 primitive 261 type 261 PrimitiveType 261 private 22 procédure 15 déclarer 16 nom 16 processus asynchrone 251 projection en perspective 264 orthogonale 265 projet 321 partager 29 planifier 83 Pong 83 pseudo-code 84 sonore, créer 132 propriété AmbientLightColor 296 Count 196 FogEnabled 298 Game 87 IsConnected 71 IsDataAvailable 253 ScrollWheelValue 71 Services 75 SpecularColor 297 Techniques 312 TitleLocation 168 pseudo-code 84 public 22 R raccourcis clavier 330 random 210 rastérisation 304, 308 ReadType 253 ReceiveData 253 rectangle 214 caméra 233 refactoriser 327 référence 93 Release 149 rendu, shader 303 renommage 327 repère, main 260 réseau 248 quitter une partie 253 ResetPosition 210, 229 réverbération 140
Role Playing Game 50 rotation 116, 263 matrice 291 point d’origine 118 S SandCastle 335 sauvegarde, StorageContainer 153 SavedGames 146 scrolling 105, 232 classe de test 106 ScrollWheelValue 71 sémantique 308 SendData 253 séparation du code 30 sérialisation 148 sérialiser 178 en binaire 178 un fichier 178 XML 179 Serializable 178 service état de la souris 201 XNA 73, 75 ServiceHelper 88, 201 session 249 set 22 SetVibration 72 shader 303 ShowGamerCard 158 SignedInGamer 158 SimulateTrialMode 160 Socket 19 solo 232 solution 321 son API SoundEffect 141 XACT 131 bonnes pratiques 144 compression 139 ininterrompu 139 lire 136, 139, 141 streaming 138 SoundBank 136 SoundEffect 141 lire musique 142 son 141 MediaPlayer (classe) 142 Zune 141 sourceRectangle 213
341
=Labat FM.book Page 342 Vendredi, 19. juin 2009 4:01 16
342
Développement XNA pour la XBox et le PC
souris 70 curseur 71 molette 71 soustraction 9 specular light 294 SpecularColor 297 speed 210 splitté 231 sprite 36, 40 afficher 37, 47 animation 108 classe 37, 45 déplacement au clavier 69 échelle 119 inversion 120 mouvement 41 ordre d’affichage 124 profondeur d’affichage 116, 124 rotation 116 sheet 108 sprite sheet 108 transformation 116 SpriteBatch 86, 99, 128, 304 SpriteEffects 116 SpriteFont 128 Spritefont 125 starter kit 26 statique méthode 265, 266 variable 209 stick analogique 72 Stopwatch 64 StorageContainer 153, 168 STR 190 stratégie en temps réel 190 streaming 138 StreamWriter 175 string 8 structure 31, 308 de contrôle 308 Vector2 39 VertexPositionNormalTexture 295 surcharge 39, 99 switch 242 swizzling 307 synchrone 154
System.Diagnostics 64 System.IO 169 System.Runtime.Serialization.For matters.Binary 178 System.XML.Serialization 179 T tableau 57 byte 191, 235 Color 215 de tableaux 307 multi-dimensionnel 57 parcourir 57, 58 tileList 233 Tales of Phantasia 37 technique 310 effet 264 passe 264 Techniques 312 test 11, 197, 249 combiner 14 condition 13 test.sav 175 Tetris 35 texte FPS (Frames Per Second) 128 optimisation 129 police de caractères 126 Spritefont 125 texture 46, 281 à cheval 291 appliquer 100 arrière-plan 105 associer à une couleur 284 coordonnées 281 couleur 318 différente selon face 289 flouter 316 n’afficher qu’une partie 283 négatif 315 netteté 315 portion 102 problème 287 redimensionner 100 répéter 283 scrolling 105 teinte, varier 112 TextureCoordinate 281 TextureEnabled 282
TextureManager 62 texturer 99, 285 this 23 throw 151 Tile 190, 201 TileType 192 TimeSpan 19, 211 TitleLocation 168 ToRadians 266 transformation 291 translation 263, 293 trial mode 160 TrueSpace 299 try … catch 158 U UnloadContent 30 Update 30, 42, 48, 71, 208, 209, 211, 214, 278, 291 using 19, 326 V ValidCoordinates 191 variable 4 aspectRatio 266 sourceRectangle 213 statique 209 vecteur 39, 41, 262 composant 307 Vector2 39, 46, 116, 222, 262 constructeur 39 Vector3 262, 273 vertex 260 couleur 279 ordre d’affichage 271, 272 shader 304, 309 VertexDeclaration 282 VertexOutput 309 VertexPositionColor 267, 279 VertexPositionColorTexture 284 VertexPositionNormalTexture 295 VertexPositionTexture 281 vibration de la manette 72 Viewport 232, 237, 241 Visual Studio 321 Viterbi 186 vitesse, déplacement du personnage 203 void 16, 17
=Labat FM.book Page 343 Vendredi, 19. juin 2009 4:01 16
Index
vue 244 affichage classique 246 dessiner 238 matrice 266 nettoyer le contenu 244 réutiliser 248 W WaveBank 136, 138 buffer 139 offset 139 while 196 while (boucle) 55 Wii Sport 35 Windows Game Library 162 Windows Media Player 142 Write 175
WriteLine 175 wWinForms 81 X XACT 131 compression 139 ADPCM (Windows) 139 XMA (Xbox 360) 139 Cue 133 formats supportés 133 réverbération 140 Sound Bank 133 streaming 138 Wave 133 Wave Bank 133 XACT Auditioning Utility 135 Xbox LIVE 152 Xbox Live 29
Xbox, BeginShowStorageDeviceSele ctor 153 Xbox 360 transfert de jeu 33 XML (eXtensible Markup Language) 146, 334 désérialiser 181 sérialiser 179 XmlIgnore 179 XNA Simple Gui 81 XNB 38 xnb 181 Z Zelda 37 Zune 27, 132, 146
343
=Labat FM.book Page 344 Vendredi, 19. juin 2009 4:01 16
Grâce au tandem Live et XNA, la programmation de jeu vidéo pour PC et Xbox 360 est accessible au plus grand nombre : il n’est plus nécessaire d’investir dans de ruineux outils pour donner libre cours à ses idées de jeux et les réaliser. Cet ouvrage permettra au lecteur de s’approprier le framework XNA 3.0, mais également de comprendre comment s’organise un projet de développement de jeu vidéo. Accéder aux dernières technologies de développement PC et Xbox 360 avec le framework XNA 3.0 Pour accompagner l’explosion du développement amateur favorisé par la plate-forme de distribution en ligne Live, Microsoft a mis au point le framework XNA pour fournir toutes les briques nécessaires à la création de jeu vidéo. Supports de référence du Live, Xbox 360 et PC sont, grâce à XNA, les deux plates-formes les plus propices pour les studios indépendants, les freelances et les particuliers qui souhaitent faire connaître, voire commercialiser, leurs réalisations.
L. Labat L. Labat Passionné par le développement et les jeux vidéo, Léonard Labat assure une veille sur les technologies Microsoft en publiant régulièrement sur son blog (http://leonard-labat.blogspot. com/). Il évolue au sein du laboratoire des technologies .Net de SUPINFO (http://www.labo-dotnet. com/).
Un manuel complet pour se lancer dans un projet de création de jeu vidéo Ce livre accompagne le lecteur, débutant ou non, dans la conduite d’un projet de jeu en C#, qu’il s’agisse de programmer des événements, de créer un environnement sonore, ou de choisir ses moteurs graphique et physique et de les exploiter. L’auteur y détaille les techniques de programmation 2D et 3D. Il explore également les techniques graphiques et sonores avancées (effets, textures, défilement, transformations, animation, éclairage, design sonore, streaming) mais aussi certains algorithmes d’intelligence artificielle, sans oublier l’inclusion du mode multijoueur en réseau ou en écran splitté.
Au sommaire XNA et son environnement • Débuter en C# • Types de données • Commenter le code • Conditions • Fonctions et procédures • Classes et espace de noms • Prise en main • EDI • Starter kit • Architecture d’un projet XNA • Créer un projet • Outils pour la Xbox 360 • Les sprites • Afficher plusieurs sprites • La classe Sprite • Gestionnaire d’images : boucles, tableaux et collections • Mesure des performances • Interaction avec le joueur • Périphériques • Services • GUI • Programmer un Pong • Pseudo-code • Création du projet • Arrière-plan, raquette, balle • Améliorer le jeu • Textures, défilement, animation • Texturer un rectangle • Scrolling • Sprites sheets • Variation de teinte • Transformations • Spritefont • Sonorisation • XACT et SoundEffect • Créer un projet sonore • Lire un son et un morceau de musique • Streaming • Design sonore • Exceptions et gestion des fichiers : sauvegarder et charger un niveau • Espace de stockage • Sérialisation • Exceptions • Gamer Services • Un éditeur de cartes • Content Importers • Version démo • Pathfinding : programmer le déplacement des personnages • Algorithme et intelligence artificielle • Implémenter l’algorithme A* • Collisions et physique • Zone de collision • Moteur physique • Mode multijoueur • Partager l’écran • Gestion des caméras • En réseau avec Live • Programmation 3D • Coordonnées, primitives, vertices, vecteurs, matrices, transformations,
À qui s’adresse cet ouvrage ? – Aux étudiants en programmation qui désirent adapter leurs connaissances aux spécificités du développement de jeu pour PC et Xbox. – Aux studios indépendants et freelances qui souhaitent passer à XNA. – À l’amateur curieux qui a choisi XNA pour développer son premier jeu.
Conception : Nord Compo
effets, projection • Caméras • Matrices de vue et de projection • Appliquer une couleur à un vertex • Plaquer une texture • Transformations des objets • Lumières • Éclairer la scène • Exploiter les modèles • Améliorer le rendu avec le High Level Shader Language • Vertex shaders et pixel shaders • Syntaxe du HLSL • Fichier d’effet • Ondulation • Textures : en négatif, netteté, flou, couleur • Annexes • Visual C# Express 2008 • La documentation.
XNA pour la Xbox et le PC
PC
Développement
XNA
Développement pour la Xbox et le
Développement
XNA pour la Xbox et le PC Premiers pas en développement de jeu vidéo
Léonard Labat