25avr 2008
Votre premier jeu avec XNA 2.0: Pong!
18:20 - Par Helmut - Articles - 14 commentaires
Voici le premier tutoriel que je vous propose: faire votre propre Pong. C'est probablement le jeu le plus simple à programmer et cela a l'avantage de servir de bonne introduction au XNA.
Je vous propose donc un petit tour d'horizon du XNA, et plus particulièrement des bases du système de rendu 2D, car ce Pong sera en 2D. Vous aurez aussi un topo sur le Content Pipeline et nous finirons avec l'ajout de sons à votre Pong en passant par un petit projet XACT.
Ce tutoriel se veut volontairement simpliste et perfectible, mais vous aurez des bases pour la suite. L'important est de vous familiariser avec XNA.
De bonnes bases en C# sont recommandées, je ne vais pas vous apprendre le C#. Si vous souhaitez apprendre à programmer en C#, je ne peux que vous orienter vers cette liste de liens et de livres.

Sommaire
- Introduction
- C'est parti
- Importer vos images : introduction au Content Pipeline
- Affichons notre premier sprite
- Afficher les barres et la balle
- Gérer les commandes et bouger les barres
- Faisons avancer cette balle
- Gestion des collisions
- Buts et score
- Du son avec XACT
- Terminé!
Introduction
Si vous avez suivi mon introduction au XNA et installé les logiciels nécessaires, alors vous êtes prêt à démarrer.
Dans mes tutoriels, j'utiliserai Visual Studio 2005 Professional mais si vous avez une version C# Express, que ce soit le 2005 ou le 2008, les menus sont strictement identiques, vous ne devriez pas vous perdre.
Allons-y, démarrer XNA Game Studio 2.0 (en tant qu'administrateur, si vous êtes sous Vista), et créez un nouveau projet via Fichier / Nouveau / Projet. Dans les types de projets, vous aurez un dépliant Visual C# qui contient une section XNA Game Studio 2.0. Là, choisissez un projet de type Windows Game (2.0) et dans le champ Nom, mettez le nom de votre projet. Celui-ci n'a pas trop d'importance et peut être changé, mettez-y "MonPong" par exemple.
Vous pouvez également spécifier un emplacement d'enregistrement autre que celui par défaut.
Notre jeu va dont être destiné à la plate-forme Windows, bien qu'il devrait tourner sans problème sur Xbox 360. Pour faire un jeu sur Xbox 360, il faut faire un nouveau projet de type Xbox 360 Game (2.0), mais cela n'est pas vraiment nécessaire. Entre un jeu Windows et Xbox 360, le code est le même. Pourquoi deux types de projets différents alors? Simplement que dans l'un, Visual Studio exécutera votre code sur votre machine, et dans l'autre, Visual Studio cherchera à l'exécuter sur votre Xbox 360. En dehors de cette particularité, si vous avez codé votre jeu correctement, tout le reste est identique.
XNA propose une option pour convertir à la volée votre projet d'une plate-forme à l'autre via le menu Projet / Create a Copy for Xbox 360 et inversement.
En général, on ne test sur 360 que lorsque l'on a un prototype stable, si possible à chaques grandes étapes du développement. Ensuite on débug sur console avant de débuter la prochaine étape sur PC.
Lorsque l'on développe pour Windows, on peut utiliser toutes les librairies contenues dans .NET mais pour Xbox 360, on doit se limiter aux classes et méthodes compatibles avec le .NET Compact Framework car la 360 embarque une version compact de la machine .NET 2. Pour savoir qu'elles classes sont "Compact Compliant", visitez la librairie MSDN du .NET 2. N'hésitez pas à en user et abuser, elle est très complète et peut vous épargner des heures de recherches ou de reflexion. Beaucoup trop de développeur débutant la néglige.
Dans l'aide, vous pourrez immédiatement identifier une classes ou méthodes compatibles avec l'icone
.
Autre astuce, dans votre Visual, à tout moment, si vous vous demandez comment fonctionne une classe qui est dans votre code, mettez le curseur sur son type (ou sur le nom d'une variable) et appuyez sur F1. L'aide MSDN sera automatiquement chargée à la page de classe concernée. Encore une fois, trop peu de personnes ont le réflexe d'appuyer sur F1.
F1 est une touche magique, peu importe ce que vous avez sélectionné dans Visual, notamment dans l'explorateur de propriété, la bonne aide apparaitra.
C'est parti
Dans votre nouveau projet, ouvrez le fichier Game1.cs, si il ne l'est pas par défaut.
Vous voici devant la classe principale de votre jeu. Elle dérive de la classe Game qui se charge de tout le boulot concernant la boucle principale de votre jeu.
Pour ceux qui se sont déjà aventuré dans la programmation d'un jeu, vous n'aurez donc pas à faire de boucle principale. La classe Game vous propose de factoriser votre code en séparant la logique du graphisme.
Voyons les méthodes que contient votre projet vide:
- Initialize : ceux qui ont fait un peu du C# ne devrait pas être dépaysé. C'est dans cette méthode que l'on fait nos initialisation de base, si nécessaire. Elle est appelée toute seule après la création de l'instance du jeu.
- LoadContent : cette méthode nous propose de charger nos contenus (graphismes, sons ou autres) ici. Elle est idéale pour de petits projets, mais pour des plus gros, on ne fera pas souvent attention à celle-ci et on aura notre propre gestionnaire de contenus. Elle est appelée toute seule au chargement de votre jeu.
- UnloadContent : l'inverse de LoadContent. Elle est destinée à la dé-allocation des ressources. Cette méthode ne sert que si l'on créé plusieurs instances de la classe Game pour un même jeu, ce qui n'arrive quasiment pas. Bien souvent on n'utilisera pas du tout cette méthode et on aura aussi notre propre système de dé-allocation.
- Update : cette méthode est appelée en boucle et est faite pour que vous gérez la logique de votre jeu à l'intérieur (interactions, déplacements, événements, gestion du pad,...). Par défaut, Update contient un code qui permet de quitter l'application en appuyant sur le bouton Back du pad 360.
- Draw : également appelée en boucle, juste après Update, cette méthode est prévue pour que vous y fassiez votre travail graphique (2D/3D).
Par défaut, vous avez aussi deux membres d'initialisés:
- graphics : instance d'un GraphicsDeviceManager. Si vous regardez, il est initialisé dans le constructeur. C'est cet objet qui permet de configurer le contexte graphique (résolution d'écran, etc). Il permet aussi d'avoir des infos sur la configuration actuelle et votre matériel.
- spriteBatch : instance de SpriteBatch. Un outil qui vous permet de faire de la 2D en deux temps trois mouvements. Vous verrez que comparé à d'autres API 2D, le gros du travail est pré-mâché. Cet objet est facultatif, mais vous en aurez besoin dans ce tutoriel.
Importer vos images : introduction au Content Pipeline
Commencez par télécharger ce pack d'images faites maison (vous aurez besoin de 7zip pour les décompresser).
Décompressez l'archive n'importe où.
Pour ajouter du contenu à vos projets, regardez votre Explorateur de solutions. Dedans, vous verrez une ligne Content, c'est ici que viendra se glisser tout votre contenu. Ajoutons nos textures. Pour cela, je vous propose de vous organiser et de créer un dossier spécial. Faites un clic droit sur Content, Ajouter / Nouveau dossier (ou Add / New folder, si votre Visual est en anglais). Nommé se dossier à votre guise, je le nommerai "textures" pour ce tutoriel. On peut maintenant ajouter nos images. Cliquez sur votre nouveau dossier textures, puis faites clic droit Ajouter / Element existant et allez chercher les images décompressées précédemment. Une fois les trois images importer, celles-ci sont disponibles pour jeu. Vous pouvez, si vous le souhaitez, effacer le dossier où vous les avez décompressé, il ne sert plus à rien et vos images ont été copiées dans votre projet.
Tout les fichiers que vous ajoutez au dossier Content passerons automatiquement par le Content Pipeline.
Voici le résultat escompté dans votre explorateur de solutions.
Regardons vos textures de plus près. Sélectionnez une des textures, peut importe laquelle, et observez ce qui se trouve dans l'Explorateur de propriétés. Le moment est venu d'en apprendre un peu plus sur le Content Pipeline.
Le Content Pipeline est un système qui est propre au XNA. Avec d'autres API ou Framework graphique, pour importer du contenu, textures, modèles 3D ou autres, vous auriez du programmer votre importateur, notamment pour les modèles. Le Content Pipeline fait tout ça pour vous, et bien plus. Il va également compiler vos fichiers dans un format spécial (.xnb) qui sera apte à être lu sur Xbox 360 et qui réduit les temps de chargement. Il fait également automatiquement le lien entre les modèles, leurs textures et leurs shaders. Vous n'aurez donc pas à coder tout un lot d'importateur et de visualisateur.
Revers de la médaille, le Content Pipeline a un nombre de formats supportés réduit.
- Formats de textures : .bmp, .dds, .dib, .hdr, .jpg, .pfm, .png, .ppm, et .tga, rien de bien particulier. Le XNA gère automatiquement les formats, que ce soit du 8 ou 32bit, avec ou sans canal alpha.
- Fichiers sonores : uniquement des fichiers .xap, qui sont des projets XACT. XACT est seule et unique manière d'importer des sons avec XNA, ne verrons cela plus tard.
- Modèles 3D : .x et .fbx, qui sont respectivement les formats de DirectX et d'AutoCAD.
- Shaders : des shaders compilés, .fx, qui sont les formats de shaders de DirectX.
- XML : tout document XML peut être importé.
Il y a donc largement de quoi faire avec ces formats. Seul le format .x de modèles pour s'avérer contraignant, puisqu'il ne gère pas les animations et n'enregistre pas les tangentes (vous n'avez pas besoin de savoir ce dont il s'agit, juste que c'est embêtant :p).
Heureusement, XNA permet de programmer nos propres importateurs.
Revenons à nos moutons et à notre Explorateur de propriétés.
Vous pouvez donc voir que XNA a automatiquement importé nos images avec l'importateur de textures. Déroulez la section Content Processor et tout en bas, vous pouvez voir la propriété Texture format. Il s'agit de la compression qui sera utilisée par votre carte graphique pour stocker cette texture dans sa mémoire. Lorsque cette propriété est à "No change", la texture sera dépourvu de toute compression, ce qui sera à peu près équivalent à la taille d'un bmp et ce peu importe que le fichier d'origine soit un jpg ou png ou autre plus petit. Heureusement, les cartes graphiques proposent des compressions matérielles, comme le DXT que vous pouvez choisir dans cette propriété. Le DXT garanti un taux de compression constant de 75% au détriment d'une très légère perte de qualité, qui ne se vera de toute façon pas si vous utilisez des images déjà compressées. En général, on vous recommande d'utiliser une compression DXT si vos textures sont destinées à des modèles 3D mais on ne vous le recommande pas si vous voulez afficher des sprites ou autres éléments 2D, afin de garantir leur finesse. Libre à vous donc de mettre DXT ou non pour ce tutoriel. J'emploierai le DXT pour mes trois textures, pour la simple raison que les images d'origines n'ont déjà pas une super qualité, autant économiser la mémoire de la carte graphique.
Toujours dans l'explorateur de propriétés d'une de vos textures, voyez la propriété Asset name. C'est le nom interne de votre texture. Vous devrez utiliser ce nom pour faire appel à celle-ci dans votre code. XNA donne un nom par défaut à vos contenus suivant leur nom de fichier, tronqué de leur extension. Si deux fichiers ont le même nom, XNA rajoute un chiffre à la suite. Vous pouvez changer ces noms à votre guise, mais pour garder un minimum de clarté, je vous recommande de les laisser tels quels.
Votre contenu est pret!
Une dernière précision autour du Content Pipeline. Sur une plate-forme Windows, vous n'êtes pas du tout obligé d'utiliser celui-ci, vous pouvez parfaitement charger vos fichiers "à l'ancienne", via les classes de .NET le permettant ou en écrivant vos propres importateurs qui font directement appel à votre arborescence de fichiers.
Sur 360, il est tout simplement interdit de charger un fichier sans passer par le Content Pipeline (auquel cas votre jeu plantera minablement). Donc de façon générale, si vous avez l'intention de faire un jeu cross-plateforme, n'utilisez que le Content Pipeline.
Affichons notre premier sprite
A ce stade, si vous lancer votre jeu (la petite flèche verte, ou plus simplement la touche F5), vous serez devant un fond bleu. Pour quitter, vous devrez cliquer la croix de la fenêtre, ou appuyer sur le bouton Back de votre pad 360, pour le peu que vous en possédiez un et qu'il soit branché.
Histoire d'être un peu plus productif, nous allons ajouter à ce comportement de pouvoir fermer notre jeu avec la touche Echap de votre clavier.
Pour ce faire, rendez-vous dans la méthode Update, vous y verrez le code qui vérifie l'appui du bouton Back. Modifions le comme suit.
// on récupère l'état du clavier KeyboardState keyboard = Keyboard.GetState(); // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboard.IsKeyDown(Keys.Escape)) this.Exit();
Pour pouvoir gérer le clavier, on créé une instance de la classe KeyboardSate qui représente un état du clavier (quelles touches sont enfoncées etc). Puis on fait appel à la méthode static GetState de la classe Keyboard pour récupérer l'état actuel du clavier. Ensuite, la méthode IsKeyDown renvoi true si la touche passé en paramètre est enfoncée.
Chargeons maintenant nos textures. Déclarons tout d'abord nos textures, en dehors de toutes méthodes, juste après le SpriteBatch par exemple.
GraphicsDeviceManager graphics; SpriteBatch spriteBatch; // pour nos textures Texture2D fond, barres, balle; Rectangle barreBleue = new Rectangle(0, 0, 32, 128), barreRouge = new Rectangle(32, 0, 32, 128);
Nous avons donc trois Texture2D. Vous remarquerez que dans le fichier bars.png, j'ai mis deux barres dans une seule texture. Il va donc falloir les dissocier. Pour cela, nous déclarons deux Rectangle qui serviront à délimiter notre texture. Nous avons une texture qui fait 64x128 pixels et chaque barre à une taille de 32x128 pixels, chaque Rectangle couvre donc une barre.
Il y a une chose importante à savoir concernant les API graphique. Dans votre tête, lorsque vous vous représenté un repère graphique, l'origine de ce repère est probablement en bas à gauche, comme en maths, or, dans les API graphique, dont XNA, l'origine du repère est en haut à gauche et l'axe Y est inversé (positif vers le bas donc), comme ci-dessous.
Nous allons maintenant charger nos textures. Dans la méthode LoadContent, ajoutez-y le code suivant.
fond = Content.Load<Texture2D>("Textures/fond_herbe"); barres = Content.Load<Texture2D>("Textures/bars"); balle = Content.Load<Texture2D>("Textures/balle");
Ceci est la méthode pour charger du contenu provenant du Content Pipeline. Pour charger un contenu, il faut utiliser la méthode statci Load de la classe Content. Cette méthode est une méthode générique qui doit prendre le type que vous cherchez à charger, Texture2D dans notre cas. La méthode prend ensuite en paramètre le chemin vers le contenu à charger. Attention, pour les noms de contenus, il s'agit de leur "Asset Name" et non des vrais nom de fichiers.
Vos textures sont maintenant prêtes à être affiché.
Petit détail concernant les textures qui vous utiliserez dans vos projets. Faites en sorte que les dimensions de vos textures soient toujours des puissances de 2 (2, 4, 8, 16, 32, 64,...). Ceci pour la simple raison que les cartes graphiques sont optimisées pour fonctionner avec des puissances de 2. Pire que cela, si vous n'utilisez pas de puissances de 2, les performances de vos jeux seront totalement exécrables suivant les cartes graphiques. Ceci n'est valable que pour les textures. Si vous avez plusieurs sprites dans une seule texture, ils peuvent avoir n'importe quelle taille, pourvu que la texture qui les contient ai les bonnes dimensions.
Trêve de commentaires, affichons le fond de notre pong. Pour cela, dans la méthode Draw, ajoutons ceci (avant base.Draw, qui doit être appelé en dernier, tout comme base.Update dans Update).
spriteBatch.Begin(); // indique que nous commençons à dessiner // on dessine notre fond spriteBatch.Draw(fond, // la texture à utiliser new Rectangle(0, 0, screenWidth, screenHeight), // le rectangle qui va dire sur quelle partie de l'écran afficher notre texture Color.White); // la composante de notre texture, à laisser à blanc pour avoir les couleurs originales spriteBatch.End(); // on a fini de dessiner, on dit à la carte graphique d'afficher le résultat
On utilise donc le SpriteBatch qui a été créé par défaut avec notre projet. SpriteBatch est une classe outil qui permet d'afficher des groupes de sprites avec les mêmes paramètres très rapidement.
Les initiés aux API 3D remarqueront surement la présence des Begin et End. Pour la petite histoire, SpriteBatch ne fait pas de la "vrai 2D". Il s'agit en fait de véritable 3D, mais plaquée contre l'écran. C'est strictement pareil que si l'on avait aplati la matrice de projection dans un API 3D, c'est d'ailleurs la méthode que l'on utilise pour faire de la 2D avec un API 3D. La méthode Draw n'affiche donc pas des sprites approprement parlé, mais des quads texturés. Car en XNA, tout n'est que 3D, via une sur couche à DirectX. Fin de l'aparté.
Pour dessiner notre fond, il faut donc lui dire sur quelle partie de l'écran l'afficher. En l'occurrence, sur tout l'écran. On spécifie donc un Rectangle qui part de l'origine et qui va jusqu'au bout des dimensions de l'écran.
Pour récupérer les dimensions de l'écran, on utilise l'instance de la classe GraphicsDeviceManager. Dans mon cas, j'ai stocké ces valeurs dans les variables screenWidth et screenHeight (respectivement la largeur et la hauteur). Ces deux variables sont des int que j'ai déclaré en tant que membres après mes Texture2D.
Pour récuperer ces valeurs, j'ai ajouté ce bout de code dans la méthode Initialise.
screenWidth = graphics.GraphicsDevice.Viewport.Width; screenHeight = graphics.GraphicsDevice.Viewport.Height;
Maintenant, il ne vous reste plus qu'à appuyer sur F5 pour admirer le résultat.
Félicitations, vous avez affiché votre première texture.
Afficher les barres et la balle
Tout d'abord, il va nous falloir des variables pour gérer la position des barres et de la balle. Déclarons les en tant que données membres.
int barreBleuePosition, // la position de la barre bleue sur l'axe Y barreRougePosition, // idem pour la rouge bleueX, // position sur X de la barre bleue, qui sera fixe rougeX; // idem Vector2 ballePosition; // la position sur X et Y de la balle, via un vecteur
Dans Pong, les barres ne bougent que sur un seul axe, Y. Inutile donc de se préoccuper de l'axe X pour les barres, mais nous avons tout de même besoin de variables pour stocker leur position sur X, histoire d'avoir un code qui s'adapte à n'importe qu'elle résolution d'écran. Pour la balle, on utilise un Vector2, qui est un vecteur possédant 2 composantes, X et Y, idéale donc pour des positions 2D, d'autant plus que l'on peut faire facilement des opérations sur des Vector2 (addition, multiplication, etc par des composantes).
Afin que notre jeu s'adapte à toutes les résolutions, nous avons encore besoins de deux variables, le rapport sur la longueur et le rapport sur la hauteur. Imaginez que votre jeu est élaboré pour une résolution native de 1024x768. Que va-t-il se passer si on essai la résolution 1280x720? L'image sera généralement tronqué et la logique de votre jeu complètement chamboulée.
Pour remédier à cela, on se débrouille dès le début pour notre code soit indépendant de la résolution. Dans notre cas, nous prenons donc une résolution de base, disons 1024x768, dont nous nous servirons pour calculer un rapport de grossissement ou de rétrécissement suivant la résolution réellement appliquée.
Déclarons donc ceci à la suite.
float rapportWidth, rapportHeight;
Et initalisons ce beau monde dans la méthode Initialise.
rapportWidth = screenWidth / 1024.0f; rapportHeight = screenHeight / (screenHeight / (float)screenWidth * 1024.0f);
Vous remarquerez que le rapport de hauteur est calculé par rapport au ratio de l'écran. Ce, afin que notre jeu s'adapte tout seul au 16/9 ou au 4/3 ou tout autre ratio.
Nous pouvons maintenant initialiser la position de nos objets, toujours à la suite dans la méthode Initialise.
barreBleuePosition = (int)((screenHeight / 2) - (barreBleue.Height / 2) * rapportHeight); barreRougePosition = barreBleuePosition; bleueX = (int)((screenWidth * 0.05) - (barreBleue.Width / 2) * rapportWidth); rougeX = (int)((screenWidth * 0.95) - (barreRouge.Width / 2) * rapportWidth); ballePosition.X = (screenWidth / 2) - (32 / 2) * rapportWidth; ballePosition.Y = (screenHeight / 2) - (32 / 2) * rapportHeight;
Pour la position initiale des barres, on les affiches au milieu de l'écran. Il faut juste prendre soin de les décaler de la moitié de la hauteur du sprite, sinon il ne seront pas complètement au milieu.
Pour la position de chaque barre sur X, on position la première à 5% du bord gauche de l'écran, et la seconde à 5% du bord droit (donc 95% du gauche). On suit la même réflexion par rapport à la largeur du sprite pour qu'elles soient centrées.
Enfin, on dispose la balle en plein centre de l'écran, toujours par rapport à sa largeur/hauteur.
Notez que l'on prend scrupuleusement en compte les rapports de la hauteur et largeur de la résolution. Ceci aura pour effet de grossir ou rétrécir les sprites pour les adapter à la résolution.
On est maintenant prêt à afficher tout ceci.
Direction la méthode Draw, à la suite de l'affichage du fond.
// on commence à dessiner, cette fois avec des modes spéciaux spriteBatch.Begin(SpriteBlendMode.AlphaBlend, // on gère la transparence SpriteSortMode.BackToFront, // on dessine de l'arrière vers l'avant de l'écran SaveStateMode.None); // mettre à none, ceci ne servira pas dans notre exemple // on dessine la barre bleue spriteBatch.Draw(barres, // la texture à utiliser new Rectangle(bleueX, // la position sur X de la barre, fixe barreBleuePosition, // la position du Y (int)(barreBleue.Width * rapportWidth), // sa largeur, en tenant compte du rapport de la résolution (int)(barreBleue.Height * rapportHeight)), // idem pour sa hauteur barreBleue, // le Rectangle qui délimite la barre bleue dans la texture des barres Color.White); // la composante, par défaut // même reflexion pour la barre rouge spriteBatch.Draw(barres, new Rectangle(rougeX, barreRougePosition, (int)(barreRouge.Width * rapportWidth), (int)(barreRouge.Height * rapportHeight)), barreRouge, Color.White); // ainsi que pour la balle, à la différence que l'on utilise son Vector2 spriteBatch.Draw(balle, new Rectangle((int)ballePosition.X, (int)ballePosition.Y, (int)(balle.Width * rapportWidth), (int)(balle.Height * rapportHeight)), new Rectangle(0, 0, balle.Width, balle.Height), Color.White); // on a fini, la carte graphique peut tout dessiner spriteBatch.End();
Les barres et la balles sont donc dessinés à leur position initiale, et avec les bonnes proportions.
Vous remarquerez que l'on a dessiné les trois sprites en même temps entre les Begin et End. Lorsque l'on fait cela, tout ce qui est dessiner est soumis aux mêmes propriétés définies dans le Begin. En l'occurrence, gestion de la transparence etc.
Autre point très important à noter, on aurait pu encadrer chaque dessin par Begin et End, mais cela est beaucoup plus lourd pour la carte graphique. En effet, il vaut mieux dire à la carte graphique de tout dessiner d'un coup que de dessiner morceaux après morceaux.
On aurait aussi pu dessiner le fond en même temps que les sprites, mais suivant les cas, il n'est pas certifié que le fond soit dessiner en dessous ou au dessus des sprites. Il est donc préférable de forcer le fond à être dessiner en premier, pour de dessiner les sprites par au dessus.
Règles générales, regroupez au maximum les dessins entre des Begin et End, et dessinez toujours en premier ce qui est le plus au fond de votre scène. Ce que vous dessinez écrase toujours ce qui est derrière.
F5 et apprécions le résultat.
Gérer les commandes et bouger les barres
La méthode Update est appelée à chaque image rendue. Suivant la complexité de votre jeu, il se peut que votre jeu tourne à 30 images par seconde (fps), comme à 60, 200 ou 10. Ces fps ne sont pas forcément fixes et peuvent varier au cours du jeu. La méthode Update n'est donc peut être pas appelée à intervalles réguliers.
Imaginons que votre jeu tourne à 60fps et que vous avez choisi que le déplacement de votre barre se fasse à hauteur de 100 pixels par seconde. On imagine alors que Update est appelée toutes les 16,66 millisecondes (1000/60) et donc, dans la méthode Update, vous ferez un déplacement de 1,66 pixels pour vos 100 par seconde.
Or, si maintenant, pour une raison quelconque, votre jeu se met à ralentir à 40fps, voir à accélérer à 100fps, il y aura donc plus ou moins d'appel à Update et votre déplacement se vera complètement ralenti ou accéléré.
Pour palier à ce problème, il y a deux solutions:
- Forcer votre jeu à tourner à 60fps au maximum.
- Faire vos calculs de déplacement en fonction du temps écoulé entre deux appels à Update.
Ici, libre à vous de préférer une solution ou une autre. Par défaut, XNA bloque les fps à 60. Mais cela n'empêche pas à votre jeu de ralentir si les fps tombent en dessous de 60.
Personnellement, je préfère utiliser la deuxième méthode. Elle est peut être un peu plus fastidieuse à mettre en place, mais le comportement de votre jeu est assuré de tourner au même rythme peu importe les performances de la machine sur laquelle il tournera, Xbox 360 incluse.
Le top du top, c'est de combiner les deux mondes, avoir des fps fixes, pour mieux prévoir le comportement de son jeu, et calculer les déplacements en fonction du temps, pour ne pas ralentir en cas de chute d'fps.
Dans ce tutoriel, on fera tout en fonction du temps.
Etape numéro 1 : calculer le temps écoulé entre deux appels à Update.
Rien de plus simple, XNA met à notre disposition cette valeur via la variable gameTime, que l'on récupère ici en secondes (dans la méthodes Update).
float tempsEcoule = (float)gameTime.ElapsedRealTime.TotalMilliseconds / 1000.0f;
Maintenant que l'on a notre temps, il nous faut calculer le déplacement associé, tout de suite après.
int deplacement = (int)((screenHeight / 2) * tempsEcoule);
Ici, on souhaite que notre déplacement se fasse à la hauteur de "la moitié de l'écran" par seconde. Ainsi, le feeling de notre jeu sera le même peu importe un ratio de 4/3 ou 16/9 (ou autre).
Nous avons désormais tous les éléments pour appliquer notre mouvement. A la suite, nous gérons les commandes.
// joueur un // si on appui sur la touche D du clavier, ou si on pouce le stick gauche vers le bas if (keyboard.IsKeyDown(Keys.D) || GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.Y < -0.5f) barreBleuePosition += deplacement; // on déplace la barre vers le bas // sinon si on appui sur E, ou stick gauche en haut else if (keyboard.IsKeyDown(Keys.E) || GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.Y > 0.5f) barreBleuePosition -= deplacement; // on monte la barre // meme reflexion pour le joueur deux, mais avec les touches Haut et Bas du clavier if (keyboard.IsKeyDown(Keys.Down) || GamePad.GetState(PlayerIndex.Two).ThumbSticks.Left.Y < -0.5f) barreRougePosition += deplacement; else if (keyboard.IsKeyDown(Keys.Up) || GamePad.GetState(PlayerIndex.Two).ThumbSticks.Left.Y > 0.5f) barreRougePosition -= deplacement;
Rien de bien compliqué donc. Chose amusante, si vous le souhaitez, vous pouvez faire jouer les deux joueurs sur le même gamepad, un sur le stick gauche, l'autre sur le droit. C'est également utile si vous n'avez pas deux gamepad.
Sous Windows, si vous souhaitez prendre en charge un gamepad autre que celui de la 360 ou tout autre matériel, il vous faut utiliser DirectInput et là, ça devient tout de suite beaucoup plus compliqué, mais possible. Par contre, si vous faites cela, oubliez tout de suite le support de la Xbox 360.
Pour avoir une compatibilité avec la 360, il faut se restreindre aux pad 360 (guitare et big button supportés) et au clavier (que l'on peut brancher en USB sur 360).
F5 et go!
Ho drame, vous remarquerez que les barres peuvent traverser l'écran. Remédions à cela, à la suite de votre précédent code.
// si la barre bleue dépasse le haut de l'écran, on la bloque à 0 if (barreBleuePosition < 0) barreBleuePosition = 0; // si elle déplace le base de l'écran (en prenant en compte sa hauteur), on la bloc (toujours en prenant en compte sa hauteur) else if (barreBleuePosition > screenHeight - (int)(barreBleue.Height * rapportHeight)) barreBleuePosition = screenHeight - (int)(barreBleue.Height * rapportHeight); // même reflexion pour la barre rouge if (barreRougePosition < 0) barreRougePosition = 0; else if (barreRougePosition > screenHeight - (int)(barreRouge.Height * rapportHeight)) barreRougePosition = screenHeight - (int)(barreRouge.Height * rapportHeight);
Voilà, nos barres ont le comportement recherché et nous en avons fini avec cette partie.
Faisons avancer cette balle
Pour commencer, nous ne souhaitons pas que la balle commence à bouger dès que le jeu est démarré. Il faut pouvoir commander son départ afin que les joueurs puissent être prêts. Pour démarre la partie, nous allons donc programmer le "top départ" sur la barre d'espace du clavier ou la touche Start du gamepad 360.
Lors d'un départ, la balle doit aller dans une direction aléatoire et à une certaine vitesse. Nous avons donc besoin d'une variable qui représente cette direction et cette vitesse. Un Vector2 est parfait pour ce job, déclarons le en tant que donnée membre (parmi vos Texture2D et autres).
Vector2 balleVitesse = Vector2.Zero;
On lui donne une vitesse initiale nulle via la propriété static de Vector2 prévu à cet effet.
Nous allons maintenant programmer le lancement de la partie. Ce bout de code va être réutilisé pour lancer une nouvelle balle à chaque point marqué. Donc pour cela, nous avons d'abord besoin de booléens qui nous indique l'état de la partie (à la suite du Vector2 d'avant).
bool started = false, // est-ce que la partie a commencé? newGame = false; // est-ce que je dois relancer la balle?
Gérons le lancement de la balle, à la suite dans votre méthode Update.
if ( (!started && // si la partie n'a pas commencé (keyboard.IsKeyDown(Keys.Space) || GamePad.GetState(PlayerIndex.One).Buttons.Start== ButtonState.Pressed) || // et que "espace" ou "start" est appuyé newGame) // ou si on a besoin d'une nouvelle balle { // on commence par placer la balle au milieu de l'écran ballePosition.X = (screenWidth / 2) - (32 / 2) * rapportWidth; ballePosition.Y = (screenHeight / 2) - (32 / 2) * rapportHeight; started = true; // on indique que la partie a commencé newGame = false; // que l'on a plus besoin de nouvelle balle // on prend un nombre au hasard, entre 0 et 3 Random rand = new Random(); int n = rand.Next(4); // on initialise la vitesse de la balle à 200 pixels par seconde (par rapport à la résolution toujours) int vitesseX = (int)(200 * rapportWidth); int vitesseY = (int)(200 * rapportHeight); // suivant le nombre tiré au hasard, on lance la balle dans une direction ou une autre (4 possibles donc) switch (n) { case 0: balleVitesse = new Vector2(vitesseX, vitesseY); break; case 1: balleVitesse = new Vector2(vitesseX, -vitesseY); break; case 2: balleVitesse = new Vector2(-vitesseX, vitesseY); break; case 3: balleVitesse = new Vector2(-vitesseX, -vitesseY); break; } }
Nous avons la direction et la vitesse de la balle, mais la balle n'avance pas encore. Il faut appliquer ce vecteur de déplacement à la position actuelle de la balle. Juste après ce code, on a ceci.
// on déplace la balle, toujours suivant le temps écoulé ballePosition += balleVitesse * tempsEcoule;
Un petit F5, et on appui sur espace ou Start. La balle avance, mais poursuit son chemin indifféremment.
Gestion des collisions
La balle doit rebondir contre les bords de l'écran et contre les barres. Commençons par gérer les bords de l'écran. Toujours à la suite du précédent code.
// si la balle dépasse le bord supérieur, on inverse la composante Y de sa vitesse (on la fait rebondir) if (ballePosition.Y < 0) balleVitesse.Y = -balleVitesse.Y; // si la balle dépasse le bord inférieur (en tenant compte de sa hauteur), on fait rebondir la balle else if (ballePosition.Y > screenHeight - (int)(balle.Height * rapportHeight)) balleVitesse.Y = -balleVitesse.Y;
La balle rebondi maintenant sur les bords mais traverse les barres. Pour gérer les collisions entres deux volumes (et donc des rectangles), XNA propose des classes toutes faites pour gérer cela, comme la classe BoundingBox que nous allons utiliser. Les BoundingBox sont des boîtes qui vont englober les objets dont vous voulez gérer les collisions. Ces boîtes sont normalement faites pour des collisions 3D, mais en faisans abstraction de la composante sur l'axe Z, on peut parfaitement gérer des collisions 2D. On défini une BoundingBox par deux points, le supérieur gauche avant et l'inférieur droit arrière. Voici un schéma pour illustrer les BoundingBox en 3D et 2D.
Voyons cela de plus près, encore une fois dans Update.
// boite pour la balle BoundingBox balleBox = new BoundingBox( new Vector3(ballePosition.X, ballePosition.Y, 0), // on défini le premier point, avec la composante Z à 0 puisque l'on fait de la 2D new Vector3(ballePosition.X + balle.Width * rapportWidth, ballePosition.Y + balle.Height * rapportHeight, 0)); // le deuxième point, attention, contrairement aux Rectangle, une BoundingBox ne demande pas la largeur et la hauteur de la boite, mais les coordonnées du point, il ne faut donc pas oublier d'ajouter les origines du premier point // même raisonnement pour les barres BoundingBox barreBleueBox = new BoundingBox( new Vector3(bleueX, barreBleuePosition, 0), new Vector3(bleueX + barreBleue.Width * rapportWidth, barreBleuePosition + barreBleue.Height * rapportHeight, 0)); BoundingBox barreRougeBox = new BoundingBox( new Vector3(rougeX, barreRougePosition, 0), new Vector3(rougeX + barreRouge.Width * rapportWidth, barreRougePosition + barreRouge.Height * rapportHeight, 0)); // puis on utilise la méthode Intersects, qui prend en paramètre une autre BoundingBox et qui renvoi true si il y a collision if (balleBox.Intersects(barreBleueBox) || balleBox.Intersects(barreRougeBox)) { // si la balle touche une des deux barres, on inverse son mouvement balleVitesse.X = -balleVitesse.X; // et on augmente sa vitesse pour pimenter le gameplay balleVitesse += balleVitesse * 0.05f; }
Le gros du travail est donc de construire les boîtes. Notez que si la balle rebondi sur une barre, on augmente sa vitesse de 5%, ceci afin de rendre le gameplay plus amusant. La vitesse des barres étant fixe, il arrivera un moment où la vitesse de la balle sera trop rapide et ce sera alors le joueur qui anticipera le mieux les mouvements de la balle qui gagnera.
Notre gestion des collisions est loin d'être parfaite (tout comme la physique de la balle), mais ce tutoriel est là uniquement pour vous donner quelques notions de bases. On ne cherchera pas à aller plus loin dans cet exemple.
Un petit F5 et votre Pong est jouable. Mais il reste à gérer le score.
Buts et score
Dans cette partie, nous allons coder les scores et le marquage de points. Ceci sera très simple car nous n'allons pas afficher le score directement dans notre jeu mais dans le titre de la fenêtre. Pourquoi? Parce que XNA ne permet pas directement d'écrire du texte à l'écran et histoire d'alléger un peu ce tutoriel. Je vous montrerai dans un autre tutoriel comment afficher du texte.
Allons-y pour le score. D'abord, déclarons des données membres.
int scoreBleu = 0, scoreRouge = 0;
Ensuite, dans notre méthode Update, rajoutons la détection de la balle qui a dépassé les barres, et donc un but.
// si la balle dépasse à gauche if (ballePosition.X < 0) { // le joueur rouge marque scoreRouge++; // et on relance la balle newGame = true; } else if (ballePosition.X > screenWidth) { // idem si la balle dépasse à droite scoreRouge++; newGame = true; }
Il ne reste plus qu'à afficher le score. Direction la méthode Draw (rien ne vous empêche de le faire dans la méthode Update, mais histoire de bien dissocier la logique du graphisme, on le fera dans Draw).
Window.Title = "Pong : " + scoreBleu.ToString() + " - " + scoreRouge.ToString();
Et voilà, le comportement de votre Pong est terminé, ou presque.
Ajoutons-y une dernière touche.
Du son avec XACT
Pour ajouter du son à votre projet, on ne peut malheureusement pas ajouter des wav ou mp3 au Content Pipeline. XNA ne prend que des projets XACT au format .xap. Pourquoi cela? Parce que cela permet des sons compatibles avec toutes les plateformes car chacune a son propre format de son. La Xbox 360 ne supporte qu'un format spéciale à elle (XMA), sous Windows XP le son passera par DirectSound et sous Vista il passera par sa couche sonore spéciale. Pour palier à toutes ces différences, Microsoft a créé XACT qui est un outil également utilisé par les professionnels.
Tout comme les autres fichiers, sous Windows, vous n'êtes pas obliger de passer par XACT et le Content Pipeline. Vous pouvez très bien utiliser directement DirectSound, mais c'est autrement plus compliqué et vous casserez encore une fois la compatibilité de votre jeu avec la 360.
Enfin, sachez que XACT ne supporte que les fichiers WAV non compressés.
Allons-y avec les sons. Téléchargez ce petit pack de sons.
Pour lancer XACT, direction le menu démarrer, et dans le repertoire de XNA Game Studio, vous en trouverez un autre nommé Tools qui contient XACT. Il faut lancer Microsoft Cross-Platform Audio Creation Tool (XACT). Une fois lancé, faites Files/New Project et enregistrez ce projet où vous le souhaitez en le nommant "Pong" (sans les guillemets).
Concernant le son, ce tutoriel sera plus une marche à suivre qu'une réelle explication. XACT étant un outil de création à part entière, il est préférable de lui consacrer un article à lui seul.
Dans l'arborescence à droite, cliquez sur Wave Banks, puis clic droit New Wave Bank. Double cliquez sur la Wave Bank fraichement créée. Une fenêtre s'est ouverte. Dedans, glissez y vos fichiers wav ou faites clic droit Insert Wave File(s) et ajoutez vos fichiers.
Sélectionnez les deux entrées de votre Wave Bank et glissez les à droite sur Sound Banks. Ouvrez la nouvelle Sound Bank, sélectionnez les deux entrées et glissez les dans le tableau du dessous ("Cue").
Voici ce à quoi votre projet XACT devrait ressembler.
Petite explication :
- Wave Banks : vos banques de fichiers originaux tout simplement.
- Sound Banks : ici, vous pouvez définir des sons qui peuvent être composés de fragment de fichiers et aussi leur appliquer des effets, ou optimiser leur volume.
- Sound Cues : servent à jouer vos sons. Ce sont en gros des déclencheur. Vous pouvez associer plusieurs son à un seul cue. Ceci aura pour effet de jouer tous les sons en même temps au déclenchement du cue.
Voilà! Sauvegardez votre projet (CTRL+S).
Retournez à votre projet XNA et ajoutez au Content Pipeline le fichier .xap de votre projet XACT. Chose importante, il faut également copiez les fichiers wav dans le dossier "Content" (le dossier! et non le Content Pipeline) de votre projet XNA.
Vous êtes fin prêt à jouer des sons dans votre jeu.
Il faut commencer par initialiser le moteur sonore et déclarer vos banques de sons. Alors déclarons le nécessaire en données membres.
AudioEngine moteurSonore; WaveBank banqueWaves; SoundBank banqueSons;
Puis, il faut initialiser tout ceci, dans la méthode Initialise.
moteurSonore = new AudioEngine("Content/Pong.xgs"); banqueWaves = new WaveBank(moteurSonore, "Content/Wave Bank.xwb"); if (banqueWaves != null) banqueSons = new SoundBank(moteurSonore, "Content/Sound Bank.xsb");
Vous vous demandez peut-être d'où sortent les fichiers xgs, xwb et xsd? Ces fichiers n'apparaissent pas dans votre dossier "Content," mais ils sont créé par XNA à la compilation grâce à vos sons et votre projet XACT. Ces fichiers auront le noms que vous leur aurez donné dans XACT. Comme nous n'y avons pas touché, ce sont les noms par défaut.
Jouons des sons maintenant!
Pour jouer un son, rien de plus simple.
// pour le son du rebond banqueSons.PlayCue("rebond"); // pour le son du but banqueSons.PlayCue("but");
Je vous laisse le soin de placer ces sons où vous le souhaitez. Vous pouvez les placer dans la méthode Update, là où les collisions sont gérées ainsi que le score.
Terminé!
Votre Pong est désormais fini. Du moins, ce tutoriel l'est!
Il y a évidement beaucoup de marge d'améliorations et le code ne prétend pas être propre ou optimal. Le but ayant été de vous familiariser avec l'outil.
En guise de challenge, vous propose de faire ceci par vous même :
- Optimiser les collisions et la physique de la balle.
- Ajouter un menu (pour le texte, vous pouvez afficher une texture avec votre texte écrit dessus).
- Ajouter un une "intelligence artificielle" pour jouer seul (astuce : il suffit que la barre suive la balle).
- Gérer un score maximum pour gagner.
Voilà qui conclu ce premier tutoriel. Vous pouvez télécharger les sources complètes de ce tutoriel ici.
Etant donné qu'il s'agit de mon tout premier article, les questions, remarques ou toutes autres réactions sont les bienvenues.
14 commentaires
Salut,
franchement continue, ça donne vraiment envie. Perso je touche qu'à l'ActionScript (HTML, PHP of course), mais tous les sites que tu donnes, et ce petit tuto, me font dire que dans quelques mois, je vais me lancer avec des amis dans un projet intéressant !
Bref, encore !
Merci, ça m'encourage pour la suite :).
Très bon tuto! Ca me rapelle mon premier Dev de jeux video, avec les problématiques de mouvements de Frame Rate de position des éléments, On si recroirait!
Mais la ou je suis Skotché c'est sur la gestion des collisions ! Vive le XNA et les BoundingBox ! Quel gain de temps !
Merci pour mon premier tutoriel XNA ! :-)
Merci pour ce tuto, belle introduction. Bien ecrit :) tu devrais continuer hihi
merci pour ce tuto ...
C'est un bon tuto qui m'a permis de découvrir XNA et de réaliser très facilement un PONG basique.
Au début, j'avoue avoir été très perturbé d'arriver à la fin du tutoriel et d'avoir en face de moi un jeu aussi superficiel qui ne gère vraiment pas bien les collisions et qui reste dans l'ensemble très buggé.
Mais après mûre réflexion, j'avoue que ça m'a permis d'aller plus loin que le cours et d'avancer petit à petit.
C'est très enrichissant.
Je le conseille à toutes les personnes motivées pour apprendre rapidement à jouer avec XNA.
Encore une fois, merci.
Bonjour,
je suis étudiant en informatique de gestion, et je suis content d'avoir trouver un tuto sur le xna aussi claire et rapide.
Il est vraiment bien écrit . il ma donner envie d'aller plus loin.
merci encore pour ton tutoriel xna, continue!
What a blog seen hardly like these blogs nice stuff i9n the blog thanks for the blog dude thanku very much....:)
take peaceful living people and makes their RV's a maximum security prison with restrictions, stipulations and rules-rules-and more rules
so pretty.You are a good teacher. Lucky student!
Super cute! My little man would look so stylin' in those!
je te remercie beaucoup frère Helmut , ce qui est magnifique dans ce tuto , la possibilité de le transformer a une application windows phone ,
je vais jeter une coup d'oeil sur tes autres article :)
merci
I "like" you on Facebook. Would love these for my oldest boy!