Introduction à XNA Express

Auteur : Alain Zanchetta, MCS, Microsoft France
Decembre 2006 - Cet article est paru dans le magazine Programmez!

En août dernier, Microsoft a annoncé l’ouverture de la Xbox 360 au développement amateur à travers la mise à disposition d’un environnement de développement gratuit appelé XNA Game Studio Express. Cette annonce a été commentée dans le numéro 81 de Programmez ! daté d’octobre 2006 et nous ne reviendrons pas dessus : l’objectif de cet article-ci est de présenter une introduction purement technique à XNA Express. Après un bref tour d’horizon du framework XNA, nous allons voir la mise en œuvre de ces principaux composants à travers deux exemples :

  • XnaPanic est un petit jeu 2D, un semblant de clone du jeu Space Panic qui existait dans les bornes d’arcade dans les années 1980 (aïe, ça ne nous rajeunit pas).

    XnaPanic

  • XnaDemo3D est un exemple de chargement et d’affichage d’un modèle 3D. La création des modèles 3D nécessite en effet l’utilisation d’outils complémentaires hors du périmètre de cet article (et hors de mes compétences).

L’environnement de développement

XNA Game Studio Express nécessite l’installation de Visual C# Express et ne peut fonctionner au-dessus de Visual Studio .NET 2005. Heureusement, Visual C# Express et Visual Studio .NET 2005 (quelle que soit la version) cohabitent très bien sur la même machine. Pour l’instant, Windows Vista n’est pas supporté comme OS de développement (il faudra attendre le SP1 de Visual Studio .NET 2005 ainsi qu’une petite mise à jour complémentaire) mais les jeux XNA fonctionnent en revanche très bien sur Vista : il suffit d’installer le runtime XNA en plus du jeu.

Remarque : A l’heure de l’écriture de cet article, XNA Game Studio Express est en version Beta 2 mais il sera peut être disponible lors de sa publication.

L’installation de XNA Game Studio Express ajoute à Visual C# Express plusieurs types de projets :

« Windows Game » et « Xbox 360 Game » Squelette de jeu pour Windows ou Xbox 360. Une classe « Game » est crée et contient les méthodes minimales à implémenter
« Windows Game Library » et « Xbox 360 Game Libray » Bibliothèque de classe, vide au départ, référençant les assemblies XNA
« Spacewar Windows Starter Kit » et « Spacewar Xbox 360 Starter Kit » Jeu complet illustrant les principaux principles de développement XNA, un exemple en quelque sorte.

Projets XNA

Comme on peut le voir, les projets sont différents entre les jeux Windows et les jeux Xbox 360… mais le code C# pourra être pratiquement identique entre les deux plates-formes à quelques différences mineures. En effet, ce n’est pas exactement la même version du .NET Framework qui est utilisée : sur PC, il s’agit du .NET Framework 2.0 et sur Xbox 360, il s’agit du .NET Compact Framework 2.0.

Créer deux projets différents lorsqu’on veut proposer un jeu à la fois sur PC et sur Xbox 360 n’est malgré tout pas une contrainte si forte que cela car il est possible de partager les sources entre les deux projets. Il suffit par exemple de :

  • réer le projet Windows en premier (plus facile à déboguer) et développer le jeu sur cette plate-forme,
  • créer un projet Xbox 360 et supprimer les fichiers existants,
  • ajouter les fichiers du projet Windows sous forme de liens : pour ceci, faire « Add existing item » sur le projet Xbox 360 et choisir l’option « Add with Link » lorsqu’on ajoute les fichiers :

Add Existing Item

Une fois les fichiers partagés (et tous les fichiers peuvent être ainsi partagés, y compris les ressources graphiques et sonores), il suffit d’utiliser les mécanismes de compilation conditionnelle pour traiter les quelques cas où le code devra différer entre la version PC et la version Xbox 360 du jeu. Dans l’exemple associé à cet article (XnaPanic), la seule ligne de code différente entre les deux plates-formes est celle-ci, liée à l’utilisation d’une méthode de la classe Enum non disponible avec le .NET Compact Framework (et donc non liée aux classes XNA elles-mêmes) :

#if XBOX            _sprites = new MultiTextureSprite[7];#else            _sprites = new MultiTextureSprite[Enum.GetValues(typeof(ManPosture)).Length];#endif

La version beta 2 de XNA Game Studio Express permet de générer des exécutables Xbox 360 mais ne permet pas encore de les tester : il faudra attendre la version définitive (prévue pour mi-décembre) de XNA Framework et une mise à jour de la Xbox 360 et de l’abonnement Xbox Live pour ceci…

Le Framework XNA

Le Framework XNA est constitué d’un ensemble de classes dont l’objectif est de faciliter le développement des jeux : son utilisation va permettre au développeur de se concentrer sur le jeu proprement dit et non sur des tâches système souvent répétitives. Par ailleurs, on verra que les classes de gestion du contenu comme les classes donnant l’accès aux graphismes 2D et 3D ont le même objectif de simplicité (mais pas nécessairement de réduction des possibilités) qui les différencient d’autres frameworks comme Managed DirectX.

L’architecture globale du framework XNA est la suivante :

Le Framework XNA

On peut distinguer plusieurs niveaux d’abstraction dans cette architecture : les composants de plus bas niveau de XNA permettent l’accès aux ressources de la machine comme la carte vidéo (via Direct 3D) ou la carte son (via XACT).

Le développeur manipule ces composants via des classes .NET localisées dans des namespaces constituant le cœur du framework XNA :

  • Microsoft.Xna.Framework.Graphics permet l’accès aux graphismes 2D ou 3D,
  • Microsoft.Xna.Framework.Audio permet la manipulation des sons,
  • Microsoft.Xna.Framework.Input permet la gestion des manettes de jeu ainsi que du clavier,
  • Microsoft.Xna.Framework.Designer contient des classes « mathématiques » très utilisées dans les jeux 3D,
  • Microsoft.Xna.Framework.Storage permet l’accès aux périphériques de stockage comme le disque dur d’un PC ou d’une Xbox 360 ou une carte mémoire.

Au dessus de ces classes basiques, le modèle d’application gère l’architecture globale du jeu et constitue incontestablement un des apports de XNA : de la même manière que le framework .NET évite au programmeur Windows de programmer explicitement une boucle de traitement des messages, les classes du modèle d’application du XNA Framework évitent au développeur de jeu de programmer sa boucle de jeu et de traiter tous les événements de bas niveau (qu’il faut traiter si on utiliser d’autres APIs) comme l’iconification du jeu ou le changement de résolution de l’écran.
Le Content Pipeline est une technologie facilitant la gestion de toutes les ressources externes dont le jeu a besoin comme les objets graphiques ou les sons.

Enfin, au plus haut niveau, les Starter Kits fournis avec XNA Game Studio Express ainsi que les bibliothèques de contenu ou de composants déjà disponibles sur Internet contribuent à l’accélération du développement des jeux.

Nous allons maintenant examiner tour à tour ces différents composants en commençant par le modèle applicatif.

Modèle applicatif

Les jeux ont quasiment toujours la même structure :

Modèle applicatif

Après une phase d’initialisation, le cœur du jeu est constitué par une boucle constituée de deux étapes :

  • une mise à jour du modèle : prise en compte des actions du (ou des) joueur(s), détection des collisions, etc…
  • l’affichage de celui-ci.

Le framework XNA prend en charge entièrement cette architecture logicielle grâce à la classe Game. Lorsqu’on crée un projet de jeu sous XNA Game Studio Express, l’essentiel du code généré est constitué par une classe dérivant de Game et dont un squelette contenant cinq méthodes est proposé :

  • initialize : permet de créer les composants non graphiques du jeu,
  • LoadGraphicsContent (appelé au sein de la méthode Game.Initialize) : chargement des ressources du jeu,
  • UnloadGraphicsContent (appelé à la fin de l’exécution) : déchargement des ressources,
  • Update : mise à jour du modèle,
  • Draw : affichage du modèle.

Le développeur d’un jeu XNA peut préciser le mode de gestion de sa boucle de jeu (méthodes Update et Draw) : il est possible de demander au framework d’appeler Update dès que l’image précédente est dessinée (« variable step game ») ou de l’appeler à intervalles fixes (« fixed step game »). L’utilisation d’une boucle constante est probablement plus facile puisqu’il n’est pas nécessaire de prendre en compte le temps réellement écoulé depuis le dernier appel et on peut donc déplacer ses objets de manière identique à chaque appel. Si la méthode Update prend trop de temps, XNA peut sauter le rendu de l’image afin d’essayer de rattraper le retard tout en conservant un nombre correct d’appels de Update. Pour permettre une gestion correcte des jeux à boucle variable, la méthode Update reçoit en paramètre le temps de jeu ce qui permet au développeur d’animer correctement son modèle.

Derrière la classe dérivant de Game, l’implémentation de la structure du jeu est laissée au libre choix du développeur. Malgré tout, il peut s’appuyer sur la notion de GameComponent… qui a été un peu réduite entre la beta 1 et la beta 2 de XNA Express . Deux classes correspondent aux composants non affichables (GameComponent) ou affichables (DrawableGameComponent) et leurs méthodes surchargeables répondent aux méthodes de la classe Game :

  • GameComponent expose les méthodes virtuelles Initialize et Update
  • DrawableGameComponent (qui dérive de GameComponent) expose les méthodes virtuelles LoadGraphicContent, UnloadGraphicContent et Draw.

Si un objet dérivant de GameComponent est ajouté à la collection Components de l’instance de la classe Game, les méthodes mentionnées ci-dessus sont appelées automatiquement par cette classe Game.

  • Un aspect important du modèle applicatif proposé par le framework XNA est une publication de services permettant une relative indépendance entre les constituants d’un jeu (classe dérivée de Game, composants, implémentation des services) :
  • Un service est identifié et récupéré par les composants du jeu à l’aide de son interface. Dans l’exemple XnaPanic, trois services sont exposés de cette manière (voir fichier IGameInterfaces.cs) :
    • ScoreService : gère la notion de « meilleur score »,
    • INumberTracerService : permet le tracé d’un nombre,
    • ISoundService : centralise la gestion des sons.
  • La classe Game possède une propriété Services de type GameServiceContainer. Elle est utilisée de la manière suivante :

// Enregistrement du serviceServices.AddService(typeof(ISoundService), _soundService);// Accès au service (depuis un composant par exemple)_soundService = (ISoundService)game.Services.GetService(typeof(ISoundService));

Bien que l’intérêt de cette approche ne saute pas nécessairement aux yeux sur un exemple basique, le découplage qu’elle assure entre les différents composants d’un jeu est fort utile dans le cas d’un développement plus conséquent, ainsi que dans tous les scénarios de réutilisation de composants ou de services.

Le Content Pipeline

Lors du développement d’un jeu, une des tâches les plus consommatrices de temps et d’énergie est la constitution puis l’incorporation de contenu graphique ou sonore :

  • Textures 2D,
  • Modèles et textures 3D,
  • Sons (musique de fond, sons pour les événements du jeu, etc).

XNA Express ne propose pas d’outils pour la création de ces contenus mais facilite leur exploitation en prenant en charge toutes les étapes nécessaires à cette exploitation : conversions éventuelles en formats binaires (éventuellement dépendants de la plate-forme), chargement et exposition au sein du jeu en tant qu’objets fortement typés et directement exploitables dans le code.

Lorsqu’on ajoute à un projet XNA Express un fichier appartenant à un des types reconnus par le Content Pipeline, ce fichier est automatiquement traité pendant les phases de compilation du projet de la même manière qu’un source C# est compilé. La ressource correspondante peut ensuite être accédée facilement par le code C#.

Pour illustrer ce mécanisme, voici la fenêtre de propriétés d’un fichier .PNG ajouté au projet :

Proprietes fichier

Le fichier MenuScreen.png est considéré comme une texture et traité comme tel. Il est ensuite accessible en C# de la manière suivante :

Texture2D textureMenu = content.Load<Texture2D>("MenuScreen");

Tous les types de contenus reconnus par le Content Pipeline sont chargés par un code aussi simple.

Le Content Pipeline sait traiter un certain nombre de types de fichiers de ressources mais il ne peut pas couvrir l’ensemble des formats disponibles. Pour compenser ceci, il est en réalité extensible et il est donc possible d’incorporer dans le même processus le traitement d’autres types de ressources.

Les formats pris en charge par le Content Pipeline de base sont :

  • Modèles 3D : fichiers .FBX, .X
  • Formats 2D : fichiers .DDS, .BMP, .JPG, .PNG et .TGA
  • Formats de matériaux : fichiers .FX
  • Formats Audio : fichiers .XAP

Remarque : le format des fichiers audio est celui de l’outil XACT (Microsoft Cross Platform Audio Creation Tool), qui sait manipuler les fichiers son basiques comme les .WAV.

Les namespaces du cœur du Framework

Graphiques

XNA Framework permet de manipuler des contenus graphiques 2D et 3D. Bien qu’il se base sur DirectX, le XNA Framework a retenu une approche différente de Managed DirectX (qui ne devrait d’ailleurs plus évoluer) : Managed DirectX est essentiellement une encapsulation .NET des interfaces COM qui constituent DirectX afin de permettre leur utilisation dans du code managé mais l’utilisation de ces interfaces ressemble plus à du code C++/COM qu’à du code .NET (par exemple, on manipule certains objets à l’aide de Handles et non de classes). Le XNA Framework offre la définition de classes et d’interfaces au standard .NET qui permettent la programmation graphique en s’appuyant sur DirectX. Il peut y avoir une légère perte de performances mais la programmation de ces classes est plus facile dans la mesure où leur conception est proche des autres classes du framework .NET.

Afficher un écran en 2D est très facile : il faut créer un objet de type SpriteBatch (il peut être créé au lancement du jeu puis conservé) et encadrer les opérations de tracé de textures 2D dans ce batch par des appels à SpriteBatch.Begin() et SpriteBatch.End(). Ceci est réalisé par la méthode Draw de la classe Game et des composants associés :

  • Création du batch (typiquement dans Game.LoadGraphicsContent) :

    _spriteBatch = new SpriteBatch(_graphics.GraphicsDevice)

  • Méthode Game.Draw:

    _graphics.GraphicsDevice.Clear();_spriteBatch.Begin(SpriteBlendMode.AlphaBlend);

    // Tracé des composants (généralement dans leur propre méthode Draw)_spriteBatch.Draw(_texture2D, _rectangle, Color.White)

    _spriteBach.End();

La method Draw du SpriteBatch utilisée dans cet exemple est la plus simple de ses différentes surcharges : elle prend en paramètres la texture à afficher, le rectangle indiquant où tracer cette texture ainsi qu’une couleur permettant d’appliquer des effets, le blanc signifiant aucun effet.

Les graphismes 3D ne sont guère plus difficiles à gérer au niveau de XNA, même s’il reste plus compliqué de créer un jeu en 3D qu’un jeu en 2D : les modèles sont plus longs à créer que de simples textures et le cœur du jeu aura des calculs plus compliqués à faire pour gérer la position des différents objets en 3D ainsi que leur orientation :

  • Chargement du modèle à l’aide du Content Pipeline :

    Model _modele;_modele = loader.Load<Model>(@"Modeles\p1_rocket");

  • Affichage du modèle, en plusieurs étapes:

    1. Copie des elements du modèle dans un tableau

      Matrix[] transforms = new Matrix[m.Bones.Count];m.CopyAbsoluteBoneTransformsTo(transforms);

    2. Création d’une perspertive : 45° de largeur de champ, ratio de 4/3, objets rendus entre une distance de 1 (Near Plan Distance) et de 10000 (Far Plan Distance)

      Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45.0f),4/3, 1.0f, 10000.0f);

    3. Création de la vue: caméra en (0,50,zoom ) , regardant vers le point (0,0,0) et avec l’axe des Z définissant le haut de l’écran

      Matrix view = Matrix.CreateLookAt(new Vector3(0.0f, 50.0f, Zoom),   Vector3.Zero, Vector3.Up);

    4. Tracé des élements du modèle

      foreach (ModelMesh mesh in m.Meshes) {    foreach (BasicEffect effect in mesh.Effects) {        effect.EnableDefaultLighting();        effect.View = view;        effect.Projection = projection;        effect.World = MyWorldRotation * mesh.ParentBone.Transform                            * Matrix.CreateTranslation(Position);    }    mesh.Draw();}

Sons

La compilation d’un fichier de XACT se traduit par la création de plusieurs fichiers, qu’XNA peut ensuite charger dans différentes classes :

  • La classe AudioEngine représente le moteur de rendu sonore :
    AudioEngine  _AudioEngine = new AudioEngine(@"Sounds\XnaPanic.xgs");
    Il faut appeler sa méthode Update() dans la méthode du même nom de la classe Game (cf. code XnaPanic).

  • La classe WaveBank représente l’ensemble des fichiers WAV ajoutés au projet XACT.
    WaveBank _WaveBank = new WaveBank(_AudioEngine, @"Sounds\Wave Bank.xwb");

  • La classe représente la collection des sons qu’on va jouer dans le courant du jeu :
    SoundBank _SoundBank  = new SoundBank(_AudioEngine, @"Sounds\Sound Bank.xsb");

    Cue ISoundService.PlaySound(Sound sound){    Cue cue = _SoundBank.GetCue(sound.ToString());    cue.Play();    return cue;}

Dans XnaPanic, un type énuméré a été créé pour permettre de désigner facilement le son à jouer lors des divers événements du jeu :

  • Jouer un son simple :
    _soundService.PlaySound(Sound.Step);
  • Jouer un son qui va durer, puis l’arrêter :
    _cue = _soundService.PlaySound(Sound.Fall);…_cue.Stop(AudioStopOptions.Immediate);

Périphériques d’entrée

Les trois types de périphériques pris en charge par XNA Express sont le clavier, la souris et les manettes de jeu (au passage, rappelons qu’on peut connecter sur un PC une manette filaire de Xbox 360). A chacun de ces périphériques correspondent (au moins) une classe représentant le périphérique et une autre capable d’en représenter l’état.

Pour savoir si une touche est enfoncée, il suffit de récupérer une instance de KeyboardState et de l’interroger :

KeyboardState keyboard = Keyboard.GetState();if (keyboard.IsKeyDown(Keys.Escape)) {…}

Les autres périphériques fonctionnent selon le même principe, mais les possibilités des manettes sont bien évidemment plus étendues. En particulier, les sticks retournent non des valeurs booléennes mais des vecteurs indiquant leur position exacte sous forme valeurs flottantes.

Dans le cas du développement d’un jeu pouvant à la fois être joué avec le clavier et avec une manette (typiquement sur un PC), le SpaceWar Starter Kit contient plusieurs classes fusionnant les deux périphériques afin que de présenter une seule interface au reste du jeu : la classe XInputHelper présente l’état des manettes de jeu… enrichi par les actions sur le clavier. Ainsi, une pression sur la touche ‘V’ simule un appui sur le bouton ‘A’ de la manette 1.
Ce code est pratiquement réutilisable tel quel dans tous les jeux pour PC où on voudra gérer ces deux types de périphérique. Il n’a pas été réutilisé dans XnaPanic afin de ne pas masquer l’utilisation des classes standards du framework XNA.

Stockage
Qu’on écrive un jeu pour PC ou un jeu pour XBox, il est souvent nécessaire d’accéder à un espace de stockage, typiquement pour conserver les meilleurs scores ou la progression d’une partie (pour les jeux un peu longs). Or le système de fichiers de la Xbox est fort différent de celui d’un PC et ne doit absolument pas être accessible directement par le programmeur d’un jeu : en effet, XNA ne doit pas constituer une faille de sécurité de la Xbox 360 pouvant rendre le disque dur de celle-ci inutilisable ! Les classes du namespace Storage permettent un accès identique à un espace réservé qu’on soit sur PC ou sur XBox : en effet, un jeu a en réalité uniquement besoin d’accéder à un espace de stockage privé dans lequel il peut écrire et lire ses propres données, et il n’a pas besoin de savoir où ni même comment est enregistré cet espace de stockage sur le disque.

A l’aide des classes de ce namespace, le développeur peut accéder à ses propres fichiers en lecture comme en écriture. L’intégration avec les classes habituelles du .NET framework est complète car les classes StorageContainer et StorageDevice n’exposent pas les méthodes de lecture & écriture de fichiers mais permettent la récupération de Streams comme les classes de System.IO que tout développeur .NET est habitué à manipuler.

L’accès à un fichier pour (par exemple) y sauvegarder le meilleur score se fait de la manière suivante :

private const string GameName = "XnaPanic";private const string FileName = "BestScore";void SaveHighScore(){    try {        StorageDevice device = StorageDevice.ShowStorageDeviceGuide();        using (StorageContainer container =                                device.OpenContainer(GameName)) {            string fullPath = Path.Combine(container.Path, FileName);            using (StreamWriter writer = File.CreateText(fullPath)) {…

Conclusion

Au travers de cet article, nous avons parcouru rapidement XNA Game Studio Express ainsi que le framework sous-jacent. Au travers d’un certain nombre d’exemples, nous avons pu voir comment ce framework remplissait son objectif initial qui est non de devenir l’outil unique de développements de jeux sur Xbox 360 mais de rendre accessible au plus grand nombre le développement de jeux pour PC ou Xbox 360.

A l’heure où cet article est écrit, XNA Express n’en est qu’à ses débuts : l’environnement est encore en beta, tout comme la mise à jour des Xbox 360 permettant l’exécution (et le débogage) des jeux XNA Express. A travers le grand nombre de blogs et de sites communautaires consacrés à XNA, on peut mesurer l’importance de l’attente des développeurs, il reste maintenant à attendre de pouvoir mettre à jour sa XBox, d’y télécharger ses jeux puis de les partager avec la communauté des autres joueurs.

Il existe des astuces permettant d’installer XNA Studio sous Vista mais certains outils complémentaires ou certaines opérations peuvent ne pas fonctionner correctement.

La beta 1 permettait de déposer les Game Components sur une surface de design, à la manière de l’éditeur de formulaires Winforms. Cette partie design a disparu mais le code des composants reste fonctionnel.

Dans cet exemple, « zoom » est une variable qui est modifiée à l’aide des flèches directionnelles, ce qui permet d’agrandir ou de réduire le modèle affiché en s’en rapprochant ou en s’en éloignant.