Astuces DotNet (Sébastien Courtois)

06/10/2010

[XNA 4] Tutoriel 3D 2 : Cube / VertexBuffer / IndexBuffer

Après avoir joué avec un triangle lors du tutoriel précédent, nous allons maintenant voir comment faire un cube coloré. Nous allons aussi étudier la gestion des vertices au sein de la carte graphique et comment optimiser l’affichage et le rendu 3d.

Nous reprendrons le résultat du dernier tutoriel pour commencer celui-ci.

  • Au commencement était le cube…

Nous allons commencer par un petit rappel sur la définition mathématique d’un cube. Un cube est une forme géométrique à 3 dimensions comprenant 6 faces, 12 arêtes et 8 sommets.

180px-Hexahedron

Comme vous pouvez le voir sur l’image ci-dessus, les faces mathématiques sont des carrés. Or XNA ne permet de définir que des triangles. Il faudra compter donc 2 triangles par face pour notre cube. Cela donnera quelque chose comme cela.

xna_tuto3d2_1

Comme indiqué lors de l’introduction de cet article, nous souhaitons réaliser un cube coloré. Chaque face ayant une couleur uni et différente des autres faces or, en XNA, chaque sommet a une position et une couleur unique. Un sommet d’un cube est commun à trois faces. Mais on ne peut pas, avec un seul sommet, assigné trois couleurs différentes. il sera donc nécessaire de créer, pour un même sommet mathématique, 3 vertices (un pour chaque face auquel il est relié). On passe donc de 8 sommets mathématiques à 36 vertices.

Nous allons créer une nouvelle méthode CreateCubeColored afin de créer l’ensemble des vertices.

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow), 
    };
}

Remarque : l’ordre des vertices est important car dépendant du backface culling expliqué dans le tutoriel précédent. L’écriture de ces définitions de vertices peut être fastidieux à la longue. Il est à noter que cela est rarement utilisé. On préfère utiliser des modeleurs 3D (type Blender, Maya ou 3DSMAX) pour réaliser des modèles fournissant directement ces données.

On modifie maintenant la méthode Initialize pour appeler la méthode CreateCubeColored.

protected override void Initialize()
{
    //this.CreateTriangle();
    this.CreateCubeColored();
    this.CreateCamera();

    base.Initialize();
}

Puis on réalise l’affichage du cube de la même façon que pour le triangle (sans changer une ligne de code).

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Vous obtenez un Cube coloré qui tourne sur lui-même.

xna_tuto3d2_2

  • Gestion de la mémoire : VertexBuffer

Lorsque nous créons un tableau de vertices, celui-ci est stocké au niveau de la RAM du microprocesseur (CPU). Lors de l’appel à la méthode DrawUserPrimitives, nous fournissons le tableau de vertices. La méthode va chercher en RAM les données pour les copier sur la mémoire de la carte graphique puis faire le rendu adéquat. Or, vu que la méthode DrawUserPrimitives est appelée 60 fois par secondes, cette copie à lieu un grand nombre de fois inutilement car les données n’ont pas changé.

Pour pallier ce problème, il existe une classe VertexBuffer chargé de copier une fois pour toute les données des vertices sur la mémoire de la carte graphique et de les réutiliser directement.

Pour réaliser créer le VertexBuffer, il suffit de modifier CreateCubeColored.

private VertexBuffer vb;

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[] { /* Données du cube */ };
    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);
}

Le constructeur du VertexBuffer prend en paramètre le lien vers la carte graphique, le type de vertex utilisé, le nombre de vertices à stocker ainsi que l’utilisation que l’on compte faire du VertexBuffer. Ce dernier paramètre peut avoir deux valeurs : None (par défaut) ou WriteOnly (optimisé pour l’écriture et la modification des données). Une fois créé, on remplit le VertexBuffer avec le tableau vertices puis on fournit le VertexBuffer à la carte graphique grâce à la méthode SetVertexBuffer en vue de la copie des données.

Au niveau de l’affichage, on va remplacer la méthode DrawUserPrimitives par DrawPrimitives.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Les deux méthodes ont une seule différence dans leur paramètre. Le deuxième paramètre de DrawUserPrimitives n’existe plus dans DrawPrimitives. Ce paramètre servait à fournir le tableau de vertices. Cela n’est plus nécessaire car DrawPrimitives va chercher les données sur les vertices dans le VertexBuffer défini précédemment.

On obtient le même résultat que précédemment si on exécute l’application.

  • IndexBuffer : Optimisation mémoire des vertices

Si vous regarder la liste des vertices décrivant le cube, vous remarquerez que certains sont redondant (le [0] et le [3], le [2] et le [4] …). XNA fournit un moyen de réduire ces redondances en ne fournissant qu’un seul vertex qui sera réutilisé par plusieurs faces. Pour cela, on a besoin de deux tableaux : un tableau de vertices “uniques” et un tableau d’index qui va permettre de lié les vertices et les faces.

int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow)
    };

    this.indices = new int[]
    {
        0, 3, 2, 0, 2, 1,
        4, 7, 6, 4,6, 5,
        8, 11, 10,8,10, 9,
        12, 15, 14, 12, 14, 13,
        16, 19, 18,16,18, 17,
        20, 23, 22,20,22, 21            
    };

}

La nouvelle méthode CreateIndexedCubeColored créé un tableau de 24 vertices uniques et un tableau d’indices liant les triangles aux vertices. Ainsi le premier triangle sera les trois premières valeurs du tableau d’indices (0,3,2). Cela permet de réduire les redondances et d’alléger la mémoire.

Méthode sans IndexBuffer : 36 vertices de 16 octets => 576 octets

Méthode avec IndexBuffer : 24 vertices de 16 octets + 144 octets d’indices => 528 octets (ou 24*16+72 => 456 octets si on utilise des short à la place de int pour les indexs).

Même si la différence ne parait pas énorme sur un cube, il faut imaginer qu’elle est très grande quand les vertices/modèles deviennent très complexe.

Pour l’affichage de données indexés, il est nécessaire comme pour les vertex d’utiliser un VertexBuffer et un IndexBuffer.

VertexBuffer vb;
IndexBuffer ib;
int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]  {  /*Vertices*/ };

    this.indices = new int[]  { /*Indices*/ };

    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);

    this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly);
    this.ib.SetData(this.indices);
    this.GraphicsDevice.Indices = this.ib;

}

L’indexBuffer se crée de la même façon que le VertexBuffer. La seule différence se situe sur le deuxième paramètre où il est nécessaire d’indiquer le type de données utilisée pour les index. Deux possibilités : 32 bits (int) ou 16 bits (short). Le choix est ouvert et dépend du nombre de vertices utilisés (short est compris entre –32768 à +32768.) et de la technologie sous-jacente. Le Windows Phone, par exemple, ne gère que des indexs 16 bits.

Une fois les VertexBuffer et IndexBuffer configurés et liés à la carte graphique, il ne reste plus qu’à afficher le nouveau cube.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        //this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
        this.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, this.vertices.Length, 0, this.indices.Length / 3);
    }
    base.Draw(gameTime);
}

On utilise cette fois la méthode DrawIndexedPrimitives. Le premier paramètre reste le type de primitives à dessiner. Le dernier paramètre correspond toujours au nombre de primitives (triangles) à dessiner.

Les 4 paramètres intermédiaires se nomment baseVertex,minVertexIndex, numVertices et startIndex et leur fonctionnement est un peu plus complexe.

Le paramètre baseVertex permet de décaler la valeur des indices d’une constante. Ainsi si on a un tableau d’indices {0,2,3} et que l’on met baseVertex à 30; durant le temps de l’appel DrawIndexPrimitives, le tableau d’indices va devenir {30,32,33}. Dans notre cube, on laisse cette valeur à 0 car nos indices sont déjà aux bonnes valeurs.

fig4

Exemple de l’affichage de deux triangles à partir d’un index Buffer (tiré  du blog de John Steed).

Prenons l’exemple ci-dessus d’un index buffer contenant deux triangles. Si l’on souhaite n’afficher que le deuxième, on mettra la valeur de startIndex à 3 (startIndex représentant la position de début du triangle dans l’indexbuffer). MinIndex représente la valeur d’index minimale utilisé (ici les valeurs d’index pour le triangle sont 3,2,0 => Minimun : 0). Enfin la plage d’index pour ce triangle est 0=>3. Par conséquent le nombre de vertices qui seront potentiellement utilisable est de 4. En effet, on prend aussi les vertices contenues dans la plage même s’ils ne sont pas utile (ici, le vertex 1 n’est pas utile pour le dessin du triangle 2).

Pour une explication peut être plus claire et en anglais sur ces paramètres, je vous conseille ce post.

  • Et les performances dans tout ça…

J’ai rajouté deux compteurs de FPS afin d’analyser les différences de performances entre les différentes méthodes. Les résultats pour le cube ont été sensiblement équivalents pour l’ensemble des méthodes. Toutefois, il ne faut pas en conclure que les VertexBuffer/IndexBuffer sont inutiles. En effet, dans le cas de modèles importants, le temps et la mémoire gagnés par ces mécanismes permet une amélioration sensible des performances.

Pour savoir la méthode qui convient le mieux à votre projet, vous n’avez pas d’autres moyens que de tester et mesurer les performances pour chacune des méthodes.

  • Conclusion

Après le triangle, nous avons vu comment créer un cube en optimisant les données fournies à la carte graphique. Dans le prochain tutoriel, nous allons créer un dé en texturant notre cube.

Code Source de ce tutoriel

18/09/2010

[XNA 4] Tutoriel 4 : Gestion des entrées : Clavier, Souris, Manette XBOX 360, Windows Phone Touch

Filed under: .NET, C# 4, Débutant, Jeux vidéos, XBOX 360, XNA — Étiquettes : , , , , , , , , , , , — sebastiencourtois @ 13:50

Jusqu’a présent, nous avons fait des applications démos tournant automatiquement. Même si certaines démos commerciales ou films d’animations peuvent être agréables à regarder, un jeu vidéo est intéressant grâce à l’interaction joueur/univers du jeu.

Code Source pour commencer ce tutoriel

Au travers de ce tutoriel, nous allons voir quatre outils d’interactions avec le joueur pour déplacer le tank du tutoriel précédent :

    • Clavier (PC & XBOX 360)
    • Souris (PC uniquement)
    • Touch (Windows Phone uniquement)
    • Manette XBOX 360 (PC / XBOX 360)

Remarques : Il est possible de brancher un clavier USB à une XBOX 360 et récupérer les entrées du clavier comme s’il s’agissait d’un pc. Pour le touch, XNA ne prend pas en charge que le toucher des écrans multitouch PC vendus dans le commerce.

Comme tout les codes de mise à jour du jeu non graphique, la gestion des interactions avec l’utilisateur se réalise dans la méthode Update() de la boucle de jeu.

  • Gestion du clavier

La première chose à faire est de récupérer l’état du clavier à un instant T. Cela se réalise par la méthode statique GetState() de la classe Keyboard

KeyboardState kbState = Keyboard.GetState();

Remarque : GetState() peut avoir un paramètre PlayerIndex (allant de Player.One à Player.Four). Cela est utilisé pour identifier la manette de l’utilisateur utilisant un Chatpad (clavier se branchant directement sur la manette). Pour le PC, on utilisera la version sans paramètre.

On récupère un structure KeyboardState contenant les informations sur les touches pressées ou non. KeyboardState contient 3 méthodes :

    • GetPressedKeys() : Méthode renvoyant un tableau des touches enfoncées (enum Keys)
    • IsKeyUp(Keys key) : Indique si la touche du clavier passée en paramètre est relaché (true si relâchée)
    • IsKeyDown(Keys key) : Indique si la touche du clavier passée en paramètre est enfoncée (true si enfoncée)

L’énumération Keys est une énumération de l’ensemble des touches du clavier (Keys.Up correspond à la flèche du haut, Keys.B à la touche B…). Il est possible d’utiliser les valeurs numériques correspondant à ces alias.

Partant des principes ci-dessus, on souhaite déplacer le tank en utilisant les touches du clavier et doubler la vitesse si, en plus on appuie sur la touche majuscule. On divisera par deux la vitesse si on appuie sur la touche control pendant le déplacement.

//Déplacement remise à 0 du déplacement
TankVelocity = Vector2.Zero;
//Vitesse maximun du tank
int TankMaximunSpeed = 2;

//Gestion du clavier
KeyboardState kbState = Keyboard.GetState();

if (kbState.IsKeyDown(Keys.Up))
    TankVelocity.Y -= TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Down))
    TankVelocity.Y += TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Left))
    TankVelocity.X -= TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.Right))
    TankVelocity.X += TankMaximunSpeed;
if (kbState.IsKeyDown(Keys.LeftShift) || kbState.IsKeyDown(Keys.RightShift))
    TankVelocity *= 2;
if (kbState.IsKeyDown(Keys.LeftControl) || kbState.IsKeyDown(Keys.RightControl))
    TankVelocity /= 2;

La variable TankMaximunSpeed est une constante définie comme la vitesse maximum du tank dans une direction donnée. TankVelocity décrit le vecteur de déplacement du tank pour l’itération courante.

  • Gestion de la souris

Comme nous l’avons fait pour le clavier, la première tâche pour récupérer l’état de la souris.

//Gestion de la souris
MouseState mouseState = Mouse.GetState();

La structure MouseState contient trois propriétés importantes :

    • Propriétés *Button : Permet de choisir un des boutons de la souris et de savoir s’il est pressé ou non (Enumération ButtonState).
    • Propriétés X, Y : Indique la position (X,Y) du curseur sur l’écran de l’ordinateur (la position en dehors de la fenêtre de jeu est transmise).
    • Propriété ScrollWheelValue : Indique le nombre d’utilisation du scroll depuis le début du jeu.(positif si scroll vers l’avant).

Si l’on souhaite déplacer le tank vers le clic de l’utilisateur, on utilisera le code suivant (le bouton gauche de la souris doit rester enfoncé pour faire avancer le tank).

if (mouseState.LeftButton == ButtonState.Pressed)
{
    Vector2 PositionMouse = new Vector2(mouseState.X, mouseState.Y);
    TankVelocity = PositionMouse - TankPosition;
    TankVelocity.Normalize();
    TankVelocity *= TankMaximunSpeed;
}

Par défaut, XNA ne gère pas les doubles clics ou les clics longs. C’est au développeur de gérer ces cas. Nous verrons, dans un prochain post, comment réaliser ces opérations particulières.

  • Gestion du Touch Windows Phone

Pour utiliser le Touch, il faut tout d’abord vérifier si le matériel peut gérer cette capacité.

//Gestion du touch
TouchPanelCapabilities touchCap = TouchPanel.GetCapabilities();
if (touchCap.IsConnected)
{
   [...]
}

Un fois la compatibilité matérielle vérifié, on récupère l’état de l’interface tactile.

TouchCollection touches = TouchPanel.GetState();

La struncture TouchCollection est un tableau de TouchLocation contenant chacune un identifiant, une position et un état TouchLocationState parmi les 4  états suivants :

    • Pressed : Un nouveau point de pression est disponible
    • Released : Un point pression existant a disparu (l’utilisateur a retiré son doigt de l’écran
    • Moved : Un point de pression existant lors de l’itération précédente est toujours là et la position a été mise à jour
    • Invalid : Problème de reconnaissance des points de pressions (souvent un nouveau point de pression qui est pris pour un point existant).

Si l’on souhaite réaliser la même chose que l’exemple précédent avec la souris, on utilisera le code suivant : (on ne prend que le premier contact tactile pour simuler un curseur de souris).

//Gestion du touch
TouchPanelCapabilities touchCap = TouchPanel.GetCapabilities();
if (touchCap.IsConnected)
{
   TouchCollection touches = TouchPanel.GetState();
   if (touches.Count >= 1)
   {
      Vector2 PositionTouch = touches[0].Position;
      TankVelocity = PositionTouch - TankPosition;
      TankVelocity.Normalize();
      TankVelocity *= TankMaximunSpeed;
   }
}
XNA_WindowsPhoneInput 

L’utilisation de l’interface Touch du Windows Phone sera décrit plus en détail dans un prochain post (notamment l’utilisation de gestures et du multitouch).

  • Gestion de la manette XBOX 360

La manette XBOX 360 est accessible au travers de l’API XNA. La  première étape consiste à voir si une ou plusieurs manettes sont connectées puis de récupérer son état. Dans notre exemple, nous ferons cela juste pour le premier joueur.

GamePadState gamepadState = GamePad.GetState(PlayerIndex.One);
if (gamepadState.IsConnected)
{
    [...]
}

La manette XBOX 360 n’est pas la seule utilisable avec XNA. Un certain nombre d’autres manettes peuvent être vu par XNA (sur PC uniquement). Afin de vérifier les éléments disponibles sur ces manettes, la classe GamepadCapabilities permet d’indiquer la présence ou non des joysticks/boutons.

GamePadCapabilities gamepadCaps = GamePad.GetCapabilities(PlayerIndex.One);

if (gamepadCaps.HasLeftXThumbStick && gamepadCaps.HasLeftYThumbStick)
    TankVelocity = gamepadState.ThumbSticks.Left * TankMaximunSpeed;

else if (gamepadCaps.HasLeftXThumbStick && gamepadCaps.HasLeftYThumbStick)
    TankVelocity = gamepadState.ThumbSticks.Right * TankMaximunSpeed;

else if (gamepadCaps.HasDPadUpButton && gamepadCaps.HasDPadLeftButton && gamepadCaps.HasDPadRightButton && gamepadCaps.HasDPadDownButton)
{
    if (gamepadState.IsButtonDown(Buttons.DPadUp))
       TankVelocity.Y -= TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadDown))
       TankVelocity.Y += TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadLeft))
       TankVelocity.X -= TankMaximunSpeed;
    if (gamepadState.IsButtonDown(Buttons.DPadRight))
       TankVelocity.X += TankMaximunSpeed;
}

Pour notre exemple, nous utilisons le joystick gauche s’il existe sinon nous utilisons le joystick droit s’il existe sinon nous utilisons la croix directionnelle (si elle existe). Vous pouvez remarquer que le GamepadState a deux méthodes IsButtonDown/IsButtonUp qui fonctionne de la même façon que le clavier.

Les joysticks (Thumbstick) ont un fonctionnement légèrement différent.La propriété ThumbSticks regroupe l’ensemble des joysticks (2 joysticks au maximum : Left et Right). Chaque joystick est un Vector2 indiquant le décalage  (X,Y) par rapport au point central. Le point central est (0,0) et les “extrémités” étant –1 et 1. On obtient donc une valeur comprise entre –1 et 1 pour chacun des axes.

Certaines manettes (la manette 360 notamment) sont équipées de moteurs pour faire vibrer celle-ci lors de certains évènements. Pour cela, on utilisera la méthode statique SetVibration de la classe GamePad.

if (gamepadCaps.HasLeftVibrationMotor && gamepadCaps.HasRightVibrationMotor)
    GamePad.SetVibration(PlayerIndex.One, 0.7, 0.25);

Les valeurs possibles pour chacun des moteurs vont de 0 (aucune vibration) à 1 (vibration maximum).

Un utilitaire disponible sur le site XNA montre plus clairement la gestion de la manette.

  • Conclusion

Vous pouvez maintenant récupérer les informations provenant de l’utilisateur et modifier l’environnement du jeu en conséquence. Cela conclut la première partie des tutoriels 2D de base sur XNA. Les prochains tutoriels auront pour thème les animations, le son, l’IA, les effets 2D et la gestion de contenu avancé (Content Pipeline) pour arriver sur la 3D.

Code Source de ce tutoriel

  • Exercices pour aller plus loin
  1. Changer le système de déplacement clavier/manette afin que les touches gauche/droite fasse tourner le tank et que les touches haut/bas fasse avancer/reculer le tank en fonction de son orientation.
  2. Faire tourner le tank dans la direction du clic de souris (ou du touch) pendant le déplacement (l’avant du tank doit toujours être “face” à sa destination).

La solution sera fournis prochainement.

16/09/2010

[XNA 4] Tutoriel 2 : Gestion des images (2D)

Filed under: .NET, Débutant, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:34

Remarque : Ce tutorial fait suite au Tutorial 1 et reprend le projet utilisé dans ce dernier.

Nous allons voir dans ce tutorial, comment charger, afficher et déplacer des images (aussi appelé sprites). Vous trouverez ici les images utilisées pour ce tutorial.

  • Ajout des images au projet

Lorsque vous souhaitez utiliser une image dans un projet XNA (que ce soit en 2D ou en 3D), il est nécessaire de l’ajouter au projet Content associé au projet XNA (Tutorial2DContent dans notre exemple).

xna_tuto2_1

Clic droit sur le projet Tutorial2DContent > Ajouter > Elément existant.

xna_tuto2_2

On sélectionne les fichiers que l’on souhaite ajouter et on clique sur le bouton “Ajouter”. Une fois que les fichiers sont ajoutés, on peut voir leurs propriétés.

xna_tuto2_3

La propriété Asset Name est le nom de l’image que nous utiliserons au sein de l’application. Vous pouvez noter qu’il n’y a pas d’extension. Vous pouvez la modifier si vous le souhaitez.

  • Chargement des images

Une fois que nous avons ajouté les images au projet, nous allons les charger au démarrage de l’application. Cela se fait généralement dans la méthode LoadContent() de la classe Game.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    //Chargement des deux textures
    this.tank = Content.Load<Texture2D>("tank_body");
    this.canon = Content.Load<Texture2D>("tank_canon");
}

Pour charger des images, il suffit d’appeler la méthode Load de la propriété Game.Content. Il est nécessaire de lui donner le type de données que l’on souhaite charger. Dans notre cas, on souhaite charger une image qui est représenté, dans XNA, par la classe Texture2D. On passe en paramètre  l’Asset Name de l’image à charger (et non pas le nom du fichier).

Le résultat du chargement est stocké dans une propriété de type Texture2D.

//Stockage de la texture du corps du tank
Texture2D tank;
//Stockage de la texture du canon
Texture2D canon;

  • Libération de la mémoire prise par les images (Optionnel)

Bien que .NET soit pourvu d’un Garbage collector nettoyant régulièrement les zones mémoires non utilisés, il est possible de faire le ménage soit même. Ainsi, si l’on souhaite libérer la mémoire prise par ces deux textures, il suffit d’aller dans la méthode UnloadContent de Game et d’appeler les Dispose() sur chacune des textures.

protected override void UnloadContent()
{
   this.tank.Dispose();
   this.canon.Dispose();
}

Rappel : La méthode UnloadContent() est appelée lorsque l’on sort de la boucle de jeu courante.

  • Affichage des images

Maintenant que nous avons chargé nos images, nous allons les afficher. Cela se passe dans la méthode Draw() de la classe Game.

//Position du tank
 Vector2 TankPosition = new Vector2(30, 40);

 protected override void Draw(GameTime gameTime)
 {
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin();
     spriteBatch.Draw(this.tank, TankPosition, Color.White);
     spriteBatch.End();
     base.Draw(gameTime);
 }

On utilise spriteBatch pour dessiner les textures à l’écran. On doit “préparer” spriteBatch avant son utilisation en appelant la méthode Begin() (nous parlerons de ses paramètres optionnels plus loin). On utilise ensuite la méthode Draw() afin d’afficher l’image. Dans sa version la plus simple, Draw comprend 3 paramètres :

  1. L’image à afficher
  2. La position de l’image par rapport au coin haut gauche de l’écran. La position est définie dans une structure Vector2. Afin de simplifier le code, on a créé une variable TankPosition pour stocker et manipuler la position de l’image.
  3. La teinte de l’image. Si l’on souhaite avoir l’image originale sans modification, on met la couleur blanche.

Une fois que l’on a dessiné l’ensemble des images souhaités, on appelle la méthode End() du spriteBatch afin que celui-ci envoie l’ensemble des informations à la carte graphique (ce comportement dépend du mode de spriteBatch choisi… voir plus loin dans cet article …).

  • Déplacement des images

Si l’on souhaite déplacer l’image, il suffit de modifier les données de la variable TankPosition entre chaque appel de la méthode Draw(). Si vous vous rappelez de la structure d’une boucle de jeu XNA, une méthode est appelé avant chaque Draw() ==> Update(). Cette méthode est destinée à faire toutes les tâches non graphiques (qui n’interagisse pas directement avec la carte graphique). Ainsi si l’on souhaite déplacer notre tank d’un pixel vers la droite et d’un pixel vers le bas, on utilisera le code suivant :

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
      this.Exit();

   TankPosition.X += 1;
   TankPosition.Y += 1;
   base.Update(gameTime);
}
  • Système de coordonnées & Origines

Par défaut, l’ensemble des entités 2D de XNA (fenêtres, sprites …) sont définies dans un système de coordonnées cartésienne orthonormé dont l’unité est le pixel.

xna_tuto2_4

Le coin haut gauche de l’entité est l’origine (0,0) et les x sont positif lorsque l’on va vers la droite. Les y sont positif quand on va vers le bas.

Il est important de noter que le point d’origine est le même que ce soit pour la position de l’entité comme pour d’autres transformations comme la rotation. Nous allons voir, dans le paragraphe suivant, qu’il est possible de modifier ce point d’origine.

  • Rotation des images

Nous allons maintenant faire tourner le tank sur lui-même. Pour cela nous allons rajouter une propriété permettant de conserver la rotation

//Rotation en degré du tank
float TankRotation = 30.0f;

Attention : La rotation 0 est l’image tel qu’importée dans XNA. De plus la rotation se fait selon l’axe du point d’origine. Si l’on souhaite faire tourner une image sur elle-même, il est nécessaire de déplacer le point d’origine au centre de l’image (Width/2, Height/2).

Pour réaliser la rotation,nous utilisons une version plus avancée de la méthode Draw du spritebatch :

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank,                                  // Texture (Image)
                     TankPosition,                               // Position de l'image
                     null,                                       // Zone de l'image à afficher
                     Color.White,                                // Teinte
                     MathHelper.ToRadians(TankRotation),         // Rotation (en rad)
                     new Vector2(tank.Width/2,  tank.Height/2),  // Origine
                     1.0f,                                       // Echelle
                     SpriteEffects.None,                         // Effet
                     0);                                         // Profondeur
    spriteBatch.End();
    base.Draw(gameTime);
}

Les deux premiers et le quatrième paramètres ont déjà été décrit précédemment. Intéressons-nous aux autres paramètres :

    • Zone de l’image : Il est possible de ne copier qu’une partie de la texture à l’écran. Dans ce cas, on indique un rectangle indiquant la partie de la texture à afficher. Si l’on souhaite afficher la texture en entier, on passe la valeur null.
    • Rotation : Dans ce paramètre, on indique la rotation que l’on souhaite appliquer à la texture. Attention, l’angle de rotation doit être donné en RADIANS. Pour vous aider,  XNA fournir une classe MathHelper avec une méthode statique convertissant les degrés en radians.
    • Origine : Ce paramètre permet d’indiquer le point d’origine de la texture pour l’ensemble des transformations (déplacement, rotation…). Par défaut, il est situé en haut à gauche mais il est possible de le modifier comme indiqué dans notre exemple (le point d’origine de notre exemple se trouve au centre de la texture afin que celle-ci tourne sur elle-même.).
    • Echelle : Il est possible d’agrandir et de réduire la taille de votre texture. La valeur 1 permet de conserver la taille originale.
    • Effet : Ce paramètre permet l’application d’un effet de renversement (renversement vertical ou horizontal).Si vous ne souhaitez pas utiliser ces effets, mettre à SpriteEffects.None.
    • Profondeur : Le dernier paramètre permet d’indiquer la profondeur à laquelle vous souhaitez mettre votre texture. Cela permet de gérer les cas de chevauchement de texture (voir paragraphe suivant).

L’ensemble des méthodes Draw() de spriteBatch sont décrites en détail dans la MSDN.

  • Superposition et transparence

Si nous faisons la même chose avec le canon du tank, on obtient le code suivant :

//Position du tank
Vector2 TankPosition = new Vector2(30, 40);
//Rotation en degré du canon
float CanonRotation = 40.0f;

protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.CornflowerBlue);
   spriteBatch.Begin();
   spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.End();
   base.Draw(gameTime);
}

Remarque : Le point d’origine pour le canon contient un –4.0f pour X. Cela provient du décalage entre le centre de la texture du tank et celle du canon. Afin d’avoir un affichage un peu plus réaliste, j’ai décalé légèrement le point d’origine du canon.

Cela semble correct mais lorsque l’on affiche le résultat :

xna_tuto2_5a

xna_tuto2_5b

Canon et Tank correctement placé

Lors de la rotation du canon, une partie de celui ci disparait sous le tank lui même.

Ce problème est du à la façon dont spriteBatch affiche les sprites. Cela peut être défini dans la méthode Begin() grâce au premier paramètre qui est l’énumération SpriteSortMode

Valeur

Description

Deferred

Valeur par défaut et la plus utilisée. Envoi les opérations à la carte graphique lors de l’appel de End(). Cela permet d’utiliser plusieurs spritesBatchs sans risquer d’avoir de conflit au niveau de la carte graphique. Dépend du paramètre profondeur de la méthode Draw.Le premier plan étant 1 et le fond 0.

BackToFront

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 1 et le fond 0.

FrontToBack

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 0 et le fond 1.

Immediate

Affiche les images en écrasant les pixels déjà présent. Ce mode est le plus rapide mais celui qui gère le moins bien les transparences et supperposition ainsi que l’utilisation de multiples spritesBatch.

Texture

Affiche les images dans l’ordre des textures. Ainsi, si plusieurs appels à Draw se font avec la même texture, l’ensemble des appels sera fait en même temps pour gagner du temps en transfert de données vers la carte graphiques.

Pour la documentation originale sur SpriteSortMode : La page MSDN.

Begin peut prendre aussi un autre paramètre pour indiquer comment il doit gérer les cas d’écrasement de pixel (vouloir dessiner un pixel qui a déjà été dessiné par une autre texture). Ce paramètre est de type BlendState et comprend 4 valeurs possibles.

Valeur

Description

AlphaBlend

Valeur par défaut. Fusion du pixel source/destination en tenant compte du canal alpha (transparence).

Additive

Ajoute les canaux RGBA entre les deux pixels

NonPremultiplied

Ajoute les canaux RGB entre les deux pixels sans tenir compte de la transparence

Opaque

La couleur du nouveau pixel écrase celle du tampon.

Pour la documentation originale sur BlendState: La page MSDN.

Pour résoudre le problème que nous avons rencontré, il y a donc deux solutions :

    • Intervertir les deux méthodes Draw()
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);            
    spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
    spriteBatch.End();
    base.Draw(gameTime);
}

    • Changer la méthode Begin
protected override void Draw(GameTime gameTime)
{
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin(SpriteSortMode.FrontToBack,null);
     spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.End();
     base.Draw(gameTime);
}

On obtient un affichage “correct” :

xna_tuto2_6

Remarque : Les petites zones bleues proviennent de mon placement tank/canon qui n’est pas parfait sur pour ces images.

  • Conclusion

Vous savez maintenant charger ,afficher et déplacement un image dans un univers 2D XNA. Dans un prochain post, nous verrons comment déplacer ces images en fonction des entrées de l’utilisateurs (souris,clavier,Manette XBOX 360, Touch Windows Phone). Le prochain post est consacré à l’affichage de texte dans la fenêtre.

Téléchargement du résultat final

  • Exercices pour tester ses compétences
  1. Faire se déplacer le char dans l’écran en le faisant rebondir sur les coins de l’écran.
    • Indice 1 : Attention au point d’origine
    • Indice 2 : Les dimensions de l’écran sont disponibles au travers de graphics.GraphicsDevice.Viewport
    • Indice 3 : Attention au pixel transparent autour du tank
  2. Faire le même exercices en gérant le couple char+canon.

Solution possible

14/09/2010

[XNA 4] Tutoriel 1 : Création et structure d’un projet XNA

Filed under: .NET, C# 4, Débutant, Jeux vidéos, XBOX 360, XNA — Étiquettes : , , , , , , , — sebastiencourtois @ 13:57

En l’honneur de la sortie de XNA 4, j’ai décidé de réaliser une série de posts sur le sujet. J’essaierais d’être le plus général possible afin que l’ensemble des codes fonctionnent sur les plateformes cibles de XNA 4 : PC, XBOX 360, Zune et le nouveau Windows Phone. Lorsque cela ne sera pas le cas, cela sera indiqué dans le titre ou dans les premières lignes des posts.

Ces tutoriels seront à destination des débutants en programmation 3D.Toutefois, il est nécessaire d’avoir des bonnes notions de C#.

  • Commençons par le début : XNA ?

D’après les forums/FAQ, XNA voudrait dire : “XNA’s Not Acronymed”. Vu comme ça on n’est pas plus avancé (un peu comme le GNU is Not Unix chez les adorateurs de pingouins) :).

XNA est une plateforme (Framework) de développement de jeu vidéo 2D/3D à destination du PC, de la XBOX 360 et d’appareils portables (Zune/Windows Phone pour l’instant). Cette plateforme est en fait une surcouche d’une API de développement de jeu vidéo utilisés dans la plupart des jeux vidéo commerciaux actuels : DirectX.

XNA a été créé pour :

  • Fournir un environnement de développement simplifié pour réaliser des applications graphique 2D/3D rapidement sans trop se préoccuper des couches plus bases (notamment Win32/DirectX qui sont assez indigeste).

  • Fournir un environnement entièrement managé pour la création de jeu vidéo (DirectX étant uniquement axé C++). Un projet de DirectX managé a été lancé par Microsoft, il y a quelque années, mais il fut arrêté au profit de XNA. Un projet communautaire a été relancé sur le même principe : SlimDX. L’idée est de fournir un wrapper directement sur les méthodes DirectX afin de permettre l’utilisation de fonctionnalités non disponibles dans XNA.

  • Permettre de déployer l’application sur plusieurs plateformes différentes (PC,XBOX 360,mobiles) sans avoir à (trop) changer le code.

Pour atteindre ces objectifs, XNA a limité les fonctionnalités disponibles. En effet, XNA ne permet d’utiliser que DirectX 9 afin d’être compatible avec l’ensemble des plateformes cibles (Il faut savoir que DIrectX9 est sorti en Août 2005  et nous sommes actuellement à DirectX 11sorti en Octobre 2009). De plus, XNA étant une plateforme managée, le garbage collector et les mécanismes internes de .NET seront activés et pourront ralentir l’application (comparé à une application DX C++). Toutefois, le niveau des applications XNA restent correcte pour la création de jeu (voir les démos présentées sur le concours Imagine Cup par des étudiants : http://www.xna-connection.com/post/Imagine-Cup-2010-Les-finalistes-section-Game-Design).

  • Fini les discours commerciaux, passons à la pratique :

Afin de développer sur XNA 4, il est nécessaire d’installer :

Une fois installé, lancez Visual Studio, puis aller dans Fichier>Nouveau Projet. Dans l’écran “Nouveau projet”, allez sur Visual Studio > XNA Game Studio 4 puis choisissez “Windows Game (4.0)”. N’oubliez pas de choisir un nom de projet (“Tutorial2D” dans ce post) puis cliquez sur OK.

xnatuto1

 

 

 

 

 

 

 

 

 

 

 

Une fois créé, vous devriez avoir l’arborescence suivante :

xnatuto2

Le projet Tutorial2D est le projet principal contenant le point d’entrée ainsi que toute les références de votre application. Il est composé de 4 fichiers :

  • Game.ico : Icone du jeu (visible dans le coin en haut à gauche de la fenêtre)
  • Game1.cs : Code du jeu (voir suite du tutorial)
  • GameThumbnail.png : Vignette représentant le jeu (visible dans le menu XBOX 360)
  • Program.cs : Point d’entrée du programme.

Le projet Tutorial2DContent est un projet dit “de contenu”. Il accueillera uniquement les ressources (textures, modèles 3D, sons…) qui seront utilisées dans le jeu.

  • Analysons le point d’entrée du jeu : Program.cs
using System;

namespace Tutorial2D
{
#if WINDOWS || XBOX
    static class Program
    {
        static void Main(string[] args)
        {
            using (Game1 game = new Game1())
            {
                game.Run();
            }
        }
    }
#endif
}

Il s’agit d’un point d’entrée classique. Le .NET lance l’application en appelant Program.Main. On crée une instance de Game1 et on appelle “le point d’entrée” du jeu. Le #if Windows || XBOX sont des commandes de préprocesseurs pour indiquer que le code qui encadré est uniquement pour PC/XBOX. Pour une application mobile, le code est différent et fera l’objet d’un prochain post.

  • Déroulement d’un programme XNA

Une fois la méthode Game1.Run() appelé dans Program.cs, XNA va rentrer dans un processus défini comme suit :

xnatuto3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • Initialize() : Zone d’initialisation du jeu. C’est ici que se trouve le chargement et la configuration des modules non graphiques.
  • LoadContent() : Zone de chargement des données du jeu (textures, effets(shaders), modèles 3D…).
  • On rentre ensuite dans une boucle infini (la boucle de jeu). A intervalle régulier (60 fois par secondes au maximum par défaut), les deux méthodes suivantes vont être appelées dans cet ordre :
    • Update() : Mise à jour des données du jeu. Gestion du clavier/souris/gamepad, déplacement des joueurs, moteur physique …
    • Draw() : Affichage de l’ensemble des données graphiques. C’est ici que l’on indique à la carte graphique ce que l’on souhaite afficher à l’écran.
  • UnloadContent() : Lorsque l’on sort de cette boucle infini (En appelant la méthode Exit()), la méthode UnloadContent() est appelée afin de libérer la mémoire des objets dont on n’a plus besoin.

 

  • Game1.cs

L’ensemble des méthodes décrites ci-dessus se retrouvent dans la classe Microsoft.Xna.Framework.Game dont dérive Game1. Il est donc nécessaire de les surcharger afin de pouvoir créer son propre code.

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

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

    protected override void Initialize()
    {
            

        base.Initialize();
    }

    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);
    }
}

Il est à noter que des appels aux classes parents sont réalisés à la fin de Initialize(), Update() et Draw(). Ces appels sont nécessaires au bon fonctionnement de l’application et doivent toujours se trouver à la fin des méthodes concernés. Par exemple, si vous commentez base.Initialize(), XNA ne fera pas l’appel à LoadContent() mais lancera la boucle de jeu directement (Update <=> Draw).

Deux propriétés sont utilisées dans cette classe :

 GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
  • Gestionnaire de carte graphique. Cette propriété permet la configuration de la carte graphiques ainsi que l’accès à celle-ci.

     

  • Cette propriété permet l’affichage de texte et de texture à l’écran.
  • Au sein de la méthode Update(), on remarque l’utilisation du GamePad.

      if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

    Ainsi, si l’on clique sur le bouton “back” (petit bouton blanc à gauche du bouton central), on sort de la boucle de jeu. La gestion des entrées utilisateurs est une des tâches à effectuer dans la méthode Update().

    Nous étudierons l’utilisation de l’ensemble de ces outils en détail dans les prochains posts.

    • Lancement de son premier jeu XNA

    Lorsque l’on lance le projet, on obtient un écran avec un fond bleu.

    xnatuto4

    Cela provient du code de la méthode Draw(). Lors de la première ligne, on demande à la carte graphique (GraphicsDevice) de vider la fenêtre et de mettre une couleur de fond CornFlowerBlue. (si vous souhaitez savoir pourquoi cette couleur en particulier est présente par défaut lorsque l’on crée un projet XNA, je vous conseille la lecture de ce post).

    • C’est déja fini ? non, ce n’est que le début.

    Maintenant que vous avez les bases sur le fonctionnement de XNA (désolé pas de code :(), les prochains posts seront dédiés à la création d’un jeu 2d multiplateforme (PC/XBOX360,Windows Phone). J’ai dans l’idée de faire un petit Space Invader mais si vous avez d’autres idées, n’hésitez pas à poster en commentaires.

    La suite de la séries XNA : Tutoriel 2 : Gestion des images

    Pour ceux qui veulent déjà faire de la 3D, des tutoriaux arriveront très bientôt. En attendant vous pouvez toujours aller sur les liens XNA suivants :

    • Français :

    http://msmvps.com/blogs/valentin/

    http://www.xna-connection.com/

    http://xna-france.com/

    http://www.xnainfo.com/

    http://msdn.microsoft.com/fr-fr/directx/default.aspx

    • Anglais :

    http://creators.xna.com/fr-FR/

    http://blogs.msdn.com/b/shawnhar/

    http://xna-uk.net/

    10/05/2010

    [Nouveautés C# .NET 4] Code Contracts : Mettre ses contrats sur des interfaces

    Filed under: .NET, C# 4, DevLabs, Hors Catégorie, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 15:30

    Ce post fait suite à une introduction sur les Code Contracts publié précedement.

    Les codes contracts permettent de spécifier et contrôler des règles au sein de votre code. Dans l’optique de la création d’API, il est interessant d’utiliser les Code Contract au sein de classe mais il est encore plus judicieux de les utiliser sur des interfaces (notamment pour les tests unitaires / IoC …). Nous allons donc voir comment réaliser des “code contracts” sur des interfaces.

    • Pour cela mettons nous en situation …

    public interfaceIPerson
    {
        void SetAge(int age);
    }

    public interfaceICustomer : IPerson
    {
        void SetId(int Id);
    }

    public classPerson: IPerson
    {
        protected int Age { get; set; }

        public void SetAge(int age)
        {
            this.Age = age;
        }
    }

    public classCustomer: Person,ICustomer
    {
        protected int Id { get; set; }
        public void SetId(int id)
        {
            this.Id = id;
        }
    }

    Nous créons deux interfaces IPerson pouvant modifier l’age d’une personne et ICustomer dérivant de IPerson (donc on pourra modifier son age aussi) où l’on pourra modifier son Identifiant client. Ces deux interfaces ont été implémenté dans deux classes Person et Customer. Si on prend le scénario de la création d’une API, le créateur de l’API aura défini et manipulera les interfaces et le développeur utilisant l’API, aura implémenté les deux classes.

    • Comment le créateur de l’API peut définir des spécifications sur les paramètres de ses interfaces ?

    Le but pour le créateur de l’API est de fournir un modèle de développement. Il peut ainsi indiquer les méthodes et les types à utiliser mais ne peut pas avoir plus de précision quand au contenu des paramètres. On a vu, qu’avec Code contract, il peut définir ces informations au sein des méthodes. Cela reviendrait à créer une classe abstrait (pas toujours pertinent surtout dans un langage où il n’y a pas d’héritage multiple comme le C#).

    Pour définir un code contract sur une interface, quatre étapes sont nécessaires :

    1. Créer une classe contrat héritant el ‘interface sur laquelle on souhaite faire le contrat.
    2. Implémenter les méthodes de l’interface dans la classe de contrat en fournissant les code contracts.
    3. Décorer l’interface avec l’attribut ContractClass en fournissant le type de la classe de contract
    4. Décorer la classe avec l’attribut ContractClassFor en fournissant le nom des interface qui sont régit par le contrat
    [ContractClass(typeof(PersonContracts))]
    public interface IPerson
    {
        void SetAge(int age);
    }
    
    [ContractClassFor(typeof(IPerson))]
    public class PersonContracts : IPerson
    {
        public void SetAge(int age)
        {
            Contract.Requires(age > 0 && age < 120, "L'age doit être compris entre 1 et 120 ans.");
        }
    }

    Le tour est joué. Ainsi pour le code exemple devient :

    [ContractClass(typeof(PersonContracts))]
    public interface IPerson
    {
        void SetAge(int age);
    }
    
    [ContractClassFor(typeof(IPerson))]
    public class PersonContracts : IPerson
    {
        public void SetAge(int age)
        {
            Contract.Requires(age > 0 && age < 120, "L'age doit être compris entre 1 et 120 ans.");
        }
    }
    
    [ContractClass(typeof(CustomerContracts))]
    public interface ICustomer : IPerson
    {
        void SetId(int Id);
    }
    
    [ContractClassFor(typeof(ICustomer))]
    public class CustomerContracts : PersonContracts,ICustomer
    {
        public void SetId(int Id)
        {
            Contract.Requires(Id > 0, "L'identifiant doit être supérieur à 0.");
        }
    }

    Le code ci dessus est fait par le créateur de l’API.

    public class Person : IPerson
       {
           protected int Age { get; set; }
    
           public void SetAge(int age)
           {
               this.Age = age;
           }
       }
    
       public class Customer : Person,ICustomer
       {
           protected int Id { get; set; }
           public void SetId(int id)
           {
               this.Id = id;
           }
       }

    Le code ci dessus est fait par l’utilisateur de l’API. Il n’a pas à se préoccuper des contrats définis dans l’API et peut définir ses propres contracts.

    09/05/2010

    [Nouveautés C# .NET 4] Code Contracts

    Filed under: .NET, C# 4, Débutant, DevLabs — Étiquettes : , , , , , , , — sebastiencourtois @ 16:12

    Code Contracts est une fonctionnalité ajouté à .NET 4 cette année mais qui a, pendant des années, été un projet DevLabs sous le nom de Spec# ou Code Contracts. L’idée est de permettre au développeur de fournir des informations sur son code au travers du code lui même.

    • Un exemple concret vaut mieux que de long discours

    Prenons un exemple d’une classe utilitaire.

    public class Division
    {
        public Division(decimal _num,decimal _denom)
        {
            this.Denominator = _denom;
            this.Numerator = _num;
        }
    
        public decimal Numerator { get; set; }
        public decimal Denominator { get; set; }
    
        public void Invert()
        {
            decimal tmpNum = this.Numerator;
            this.Numerator = this.Denominator;
            this.Denominator = tmpNum;
        }
    
        public decimal Compute()
        {
            return this.Numerator / this.Denominator;
        }
    
        public static decimal StaticCompute(decimal numerator, decimal denominator)
        {
            return numerator / denominator;
        }
    }
    
    public class Absolu
    {
        public static decimal StaticCompute(decimal value)
        {
            return Math.Abs(value);
        }
    }

    La première classe de division permet de créer des fractions et d’éxécuter le résultat. Un méthode statique permet de faire la même opération sans créer une instance de la classe. Une deuxième classe qui permet, au travers d’une méthode statique, de récupérer la valeur absolu d’un nombre.

    Si vous vous rappelez vos cours de mathématiques, vous savez surement qu’une valeur interdite dans le calcul d’une division est la valeur 0 pour le dénominateur. Si on regarde la méthode division, on voit que la valeur Dénominator peut être changé par le constructeur, par la propriété elle même (public get;set;), par la méthode Invert (si le numérateur = 0, alors l’invert donne un dénominateur = 0. Une solution avec .NET 3.5 serait de mettre un condition dans le set du numérateur afin de lancer une exception si une valeur = 0 tente d’être assigné. Toutefois cette façon de faire peut entrainer des codes dans les set assez long car il peut y avoir d’autres processus dans ce set ( NotifyPropertyChanged en WPF par exemple).

    • Contract Invariant / PréConditions (Requires)

    Afin de sortir le code de validation des propriétés, Code Contract propose de créer une méthode dite “Invariant” où l’on mettra toute les conditions à vérifier lorsqu’une méthode ou un constructeur est exécuté. Pour la classe Divsion, on pourrait créer la méthode “Invariant” suivante :

    [ContractInvariantMethod()]
    protected void DivisionInvariant()
    {
       Contract.Invariant(this.Denominator != 0, "Le dénominateur doit toujours être différent de 0.");
    }

    Ainsi si on tente de créer une instance Division d = new Division(2,0), on obtient l’exception suivantes

    cc1jpg

    Il est possible de faire cela sur des méthodes pour vérifier les paramètres par exemple.

    public static decimal StaticCompute(decimal numerator, decimal denominator)
    {
        Contract.Requires(denominator != 0, "Le dénominator doit être différent de 0.");
        return numerator / denominator;
    }

    Remarque : Le Contract.Requires fonctionne aussi pour les méthodes non statique.

    • PostConditions (Ensures)

    Les méthodes ci-dessus permettent de vérifier les entrées. Il serait aussi intéressant de définir les sorties. Toujours grâce à vos rappels de mathématiques, vous savez que la méthode absolu renverra toujours une valeur supérieure ou égale à 0. Cela est une spécification d’une sortie de méthode. Ainsi la classe absolu ci-dessus, devient :

    public class Absolu
    {
        public static decimal StaticCompute(decimal value)
        {
            Contract.Ensures(Contract.Result<decimal>() >= 0);
            return Math.Abs(value);
        }
    }

    La méthode Ensures() indique que la condition qui suit est satisfait lors de la sortie de la méthode. La méthode Result() récupère le résultat afin de l’analyser. Il est aussi possible de vérifier l’état d’un paramètre au début de la méthode gràce à la méthode Contract.OldValue().

    Si on remplace return Math.Abs(value) par return –1;, on obtient l’exception suivante :

    cc2

    • Installation et Configuration

    Afin d’utiliser les code contracts, il est nécessaire d’installer les outils Code Contracts : http://msdn.microsoft.com/fr-fr/devlabs/dd491992(en-us).aspx

    Une fois installé, il est nécessaire d’aller dans les propriétés du projet pour activer Code Contracts :

    cc3

    L’écran ci-dessus dépend de votre version de Visual Studio.

      • VS Express 2010 : Aucune possibilité de d’activer Code Contracts
      • VS Pro 2010 : Runtime Checking  (Standard Edition)
      • VS Team System 2008 ou VS Premium / Ultimate 2010 : Runtime + Static Checkin.  (Premium Edition)

    Juusqu’à maintenant, nous avons vu le Runtime Checking. Les vérifications se font lors de l’éxécution et génère des exceptions. Le Static Checking permet de voir ces problèmes dès la compilation lors d’une analyse statique de code.

    • Fonctionnement de Code Contracts

    Les codes de vérifications Code Contracts ne sont pas utilisé uniquement à l’exécution mais aussi à la compilation. Lors de la compilation ,le compilateur analyse les codes et génère du IL pour l’insérer aux endroits nécessaire. Ainsi la méthode Compute de Division est représenté comme suit :

    cc4 En Haut : Code généré sans code contract / En Bas : Code généré  avec Code Contracts

    Il est donc possible de mettre ses codes Contracts.Requires/Contracts.Ensures dans n’importe quel ordre dans les méthodes car tout est reclassé lors de la compilation.

    21/04/2010

    [Nouveautés .NET 4] Le Tuple

    Filed under: .NET, C# 4, Débutant — Étiquettes : , , , , — sebastiencourtois @ 10:05

    La classe Tuple est une nouvelle classe de base du Framework .NET 4. Cette classe permet de stocker des données ayant “une relation logique” au sein d’une application.

    On peut stocker 1 à 7 valeurs au sein d’un tuple grâce aux classes suivantes :

    public class Tuple<T1>
    
    public class Tuple<T1, T2>
    public class Tuple<T1, T2, T3>
    public class Tuple<T1, T2, T3, T4>
    public class Tuple<T1, T2, T3, T4, T5>
    public class Tuple<T1, T2, T3, T4, T5, T6>
    public class Tuple<T1, T2, T3, T4, T5, T6, T7>

    Afin d’utiliser un tuple, il suffit de créer une instance en fournissant les types que l’on souhaite stocker en paramètres générique de la classe. On obtient ainsi une classe typés ce qui permet d’éviter les problèmes de performances liés aux boxing/unboxing.

    Tuple<string, int> tsi = new Tuple<string, int>(string.Empty, 0);
    Tuple<string, int,bool> tsib = new Tuple<string, int,bool>(string.Empty, 0,true);

    Un tuple s’instancie comme une classe normale. On fait appel au constructeur en fournissant les données que l’on souhaite stocker conjointement. Il est intéressant de noter que, gràce à la généricité, on peut imbriquer les tuples et ainsi stocker théoriquement une infinité de données dans un tuple (voir exemple suivant).

    Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> tsib3 =
    new Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> (string.Empty, 0,true,2,string.Empty,true,2,new Tuple<int,string>(2,"Toto"));

    L’accès au données se réalise par une propriété ItemX (avec X étant le numéro d’ordre de la donnée) en lecture seule.

    public class Tuple<T1, T2> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple
    {
        public Tuple(T1 item1, T2 item2)
        public T1 Item1 { get; }
        public T2 Item2 { get; }
    }

    • Pourquoi Tuple expose ses propriétés en lecture seule ?

    La raison principale est l’immutabilité. On dit qu’une classe est “immutable” si on ne peut modifier ses données qu’au travers de son constructeur. Un des intérêts est que, dans le cas de programmation parralèlle, une classe immutable ne peut être l’objet de verrou ou de race situation car ses données sont figés dans le RAM après la création de la classe. C’est donc uniquement pour des soucis de performances que ces propriétés sont en read only.

    Il est toutefois possible de se créer une classe Tuple modifiable en créant la même classe et en rajoutant l’accesseur set sur les propriétés

    public class MyTuple<T1, T2>
    {
        public MyTuple(T1 t1, T2 t2) { }
    
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }
    • Cas d’utilisation des tuples : Les valeurs retours de méthodes

    Examinons un exemple pour comprendre une des utlisations des tuples. Je souhaite créer une méthode prenant des données sur une rectangle en paramètres et me fournissant les données sur les coordonnées de deux points de ce rectangle.

    En .NET 3.5, je pourrais faire une première méthode avec le mot clé out :

    static void CreateRectangleData(string name, int x, int y, int sizex, int sizey,out string finalname,out int p1x,out int p1y,out int p2x,out int p2y)
    {
        finalname = name;
        p1x = x;
        p1y = y;
        p2x = x + sizex;
        p2y = y + sizey;
    }

    L’utilisation de cette méthode peut être fastidieuse car il faut créer l’ensemble des variables du coté de l’appelant. De plus, il serait plus propre de créer une classe intermédiaire pour stocker/structurer ces données.

    public class MyRectangle
    {
       public string name;
       public int Point1X;
       public int Point1Y;
       public int Point2X;
       public int Point2Y;
    }

    static MyRectangle CreateRectangleData(string name,int x,int y,int sizex,int sizey)
    {
         return new MyRectangle()
         {
           name = name,
           Point1X = x,
           Point1Y = y,
           Point2X = x+sizex,
           Point2Y = y+sizey
        };
    }

    Meilleure solution (selon moi) pour récupérer ces données. Toutefois, lorsque l’on a trop de classe intermédiaire de ce genre (ne servant souvent qu’à récupérer plusieurs données d’une seul méthode) le code devient vite illisible. C’est là où les tuples peuvent intervenir.

    static Tuple<string,int,int,int,int> CreateRectangleData(string name, int x, int y, int sizex, int sizey)
    {
       return new Tuple<string, int, int, int, int>(name,x,y,x + sizex,y + sizey);
    }

    Le code devient donc plus propre car on peut se débarrasser des classes intermédiaires en les remplaçant par des tuples. Il faut toutefois bien être conscient de l’ordre des données afin de ne pas se tromper en les traiter après.

    • Avant j’utilisais KeyValuePair<K,T> et cela marchait très bien. Pourquoi utiliser Tuple<T1,T2> à la place ?

    Il faut, tout d’abord, savoir que Tuple est une généralisation de la classe KeyValuePair. On peut décrire KeyValuePair comme étant un Tuple à 2 valeurs alors que Tuple peut prendre N valeurs.

    Une différence notable est que Tuple est une classe (type référence) alors que KeyValuePair est une structure (type valeur).

    public class Tuple<T1, T2> : /**/
    {
        public Tuple(T1 item1, T2 item2)
        public T1 Item1 { get; }
        public T2 Item2 { get; }
    }

    public struct KeyValuePair<TKey, TValue>
    {
        public KeyValuePair(TKey key, TValue value);
        public TKey Key { get; }
        public TValue Value { get; }
    }

    Définition de la classe Tuple Définition de la structure KeyValue Pair

    Le choix entre KeyValuePair<K,T> et Tuple<T1,T2> doit aussi être fait selon la logique de votre code. Il est plus logique de stocker des données d’un dictionnaire selon un système de clé/valeur en utilisant KeyValuePair  et d’utiliser Tuple<T1,T2> pour stocker les données d’une méthode ayant deux valeurs retours.

    19/04/2010

    [Nouveautés .NET 4] Dynamic VS Réflexion VS Code classique : Performances

    Filed under: .NET, C# 4, Débutant, Intermediaire, Optimisation — Étiquettes : , , , , , , — sebastiencourtois @ 10:28

    Suite à des discussions sur le sujet des performances avec les Dynamics, j’ai décidé de monter le petit test suivant :

    class Program
       {
           static void Main(string[] args)
           {
               Test t = new Test();
               dynamic d = t;
               //Premier appel
               Console.WriteLine("Premier appel");
               
               //Appel direct
               Stopwatch sw = Stopwatch.StartNew();
               t.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel direct : {0}",sw.Elapsed);
               //Appel reflexion
               sw.Reset();
               sw.Start();
               Test.CallMethod<Test>(t, "TotoNoParam", null);
               sw.Stop();
               Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
               //Appel dynamique
               sw.Reset();
               sw.Start();
               d.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);
    
               //Second appel
               Console.WriteLine("Second appel");
    
               //Appel direct
               sw.Reset();
               sw.Start();
               t.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel direct : {0}", sw.Elapsed);
               //Appel reflexion
               sw.Reset();
               sw.Start();
               Test.CallMethod<Test>(t, "TotoNoParam", null);
               sw.Stop();
               Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
               //Appel dynamique
               sw.Reset();
               sw.Start();
               d.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);
    
               //Appel chainés
               //Appel direct
               for (int i = 1; i < 10000000; i *= 10)
               {
                   sw.Reset();
                   sw.Start();
                   for (int j = 0; j < i; j++)
                       t.TotoNoParam();
                   sw.Stop();
                   Console.WriteLine("Appel direct x {0} \t: {1}",i, sw.Elapsed);
               }
    
               //Appel reflection
               for (int i = 1; i < 10000000; i *= 10)
               {
                   sw.Reset();
                   sw.Start();
                   for (int j = 0; j < i; j++)
                       Test.CallMethod<Test>(t, "TotoNoParam", null);
                   sw.Stop();
                   Console.WriteLine("Appel reflection x {0} \t: {1}", i, sw.Elapsed);
               }
    
               //Appel dynamic
               for (int i = 1; i < 10000000; i *= 10)
               {
                   sw.Reset();
                   sw.Start();
                   for (int j = 0; j < i; j++)
                       d.TotoNoParam();
                   sw.Stop();
                   Console.WriteLine("Appel dynamic x {0} \t: {1}", i, sw.Elapsed);
               }
    
               Console.WriteLine("Fini");
               Console.ReadLine();
           }
    
       }

    La classe Test est la même que le poste précédent. J’ai d’abord voulu tester les performances pour un appel d’une méthode sans paramètres avec 3 méthodes :

    • Appel direct (classique depuis .NET 1)
    • Appel en utilisant la réflexion (MethodInfo.Invoke)
    • le mot clé dynamic.

    Voici les tableaux de résultats ( les graphiques ne rendant pas correctement les valeurs, j’ai choisi de ne pas en mettre).

    Sur un appel (en ms) Direct Réflexion Dynamics
    Premier appel 0,0622 0,4020 26,0880
    Second appel 0,0007 0,0076 0,7007
      88x 53x 37x

    Premier constat, un deuxième appel est toujours plus rapide que le premier quelque soit la méthode. Cela est dû à une optimisation de .NET qui compile le code à la volée et le met en cache. Le premier appel subit donc la compilation + mise en cache. Cela ajoute un temps (non négligeable) supplémentaire pour l’exécution de la méthode.

    Deuxième constant, on remarque que Direct est 6 à 10 x plus rapide que la réflexion et 400 à 1000x fois plus rapide que Dynamics.

    Si l’on regarde les apples multiples, on obtient le tableau suivant (en secondes) :

    Appels Multiples Direct Réflexion Dynamics Dynamics/Direct Réflexion / Direct Réflexion/Dynamics
    1,00 0,0000003 0,0000069 0,0007376 2 458,67 23,00 0,01
    10,00 0,0000003 0,0000238 0,0000015 5,00 79,33 15,87
    100,00 0,0000011 0,0002062 0,0000072 6,55 187,45 28,64
    1 000,00 0,0000103 0,0020013 0,0000691 6,71 194,30 28,96
    10 000,00 0,0000979 0,0211307 0,0006662 6,80 215,84 31,72
    100 000,00 0,0009323 0,1582175 0,0067310 7,22 169,71 23,51
    1 000 000,00 0,0096497 1,5509957 0,0872035 9,04 160,73 17,79

    Remarque : Afin de clarifier les choses, ces tableaux a été généré plusieurs fois afin d’être sur de la validité des résultats.

    On peut voir que l’appel classque reste, de loin, la méthode la plus rapide. Direct reste environ 5 à 10 x plus rapide que dynamic et 80 à 200 x plus rapide que la réflexion. On remarque aussi que la reflexion est plus lente (15 à 30x) que Dynamic (ce qui est en contradiction avec le premier tableau). De plus, le premier appel multiple pour Dynamic est étonnamment élevé car il intervient APRES deux appels à cette méthode issus de l’expérience précédente donc il ne devrait plus y avoir de compilation/mise en cache.

    Je n’ai pas encore trouvé les raisons de ces “incohérences”. Si vous avez des explications, n’hésitez pas à commenter ce post.

    Concernant les appels de méthodes avec paramètres, on se retrouve avec les mêmes plages de valeurs (Direct 5 à 10x plus rapide que reflexion et 500 à 1200 x plus rapide que dynamic). On retrouve les mêmes incohérences du deuxième tableau.

    Conclusion :

    L’utilisation du mot clé dynamic peut avoir des conséquences dramatiques sur les performances d’un code C# et ne doit donc être utilisé que dans les cas vraiment spécifiques comme l’interopérabilité avec un langages dynamique.

    EDIT : 28 / 07 / 2010 : Suite à un commentaire, je publie ici le code ainsi qu’une optimisation proposé pour le test :

    class Program
        {
            static void Main(string[] args)
            {
                Test t = new Test();
                dynamic d = t;
    
                Console.WriteLine("Test Appel Direct");
                Stopwatch sw = new Stopwatch();
                for (int i = 1; i < 10000000; i *= 10)
                {
                    sw.Reset();
                    sw.Start();
                    for (int j = 0; j < i; j++)
                        t.TotoNoParam();
                    sw.Stop();
                    Console.WriteLine("Appel direct x {0} \t: {1}", i, sw.Elapsed);
                }
    
                Console.WriteLine("Test Appel Reflection");
                for (int i = 1; i < 10000000; i *= 10)
                {
                    sw.Reset();
                    sw.Start();
                    for (int j = 0; j < i; j++)
                        Program.CallMethod<Test>(t, "TotoNoParam");
                    sw.Stop();
                    Console.WriteLine("Appel Reflection x {0} \t: {1}", i, sw.Elapsed);
                }
    
                Console.WriteLine("Test Appel Reflection (optimized)");
                Type type = t.GetType();
                MethodInfo mi = type.GetMethod("TotoNoParam");
                for (int i = 1; i < 10000000; i *= 10)
                {
                    sw.Reset();
                    sw.Start();
                    for (int j = 0; j < i; j++)
                        mi.Invoke(t,null);
                    sw.Stop();
                    Console.WriteLine("Appel Reflection (optimized) x {0} \t: {1}", i, sw.Elapsed);
                }
    
                Console.WriteLine("Test Appel Dynamic");
                for (int i = 1; i < 10000000; i *= 10)
                {
                    sw.Reset();
                    sw.Start();
                    for (int j = 0; j < i; j++)
                        d.TotoNoParam();
                    sw.Stop();
                    Console.WriteLine("Appel Dynamic x {0} \t: {1}", i, sw.Elapsed);
                }
                Console.ReadLine();
            }
    
            public static void CallMethod<T>(T instance, string methodName) where T : class
            {
                Type t = instance.GetType();
                MethodInfo mi = t.GetMethod("TotoNoParam");
                if (mi == null)
                    throw new InvalidOperationException(string.Format("La méthode {0} n'existe pas pour le type {1}", methodName, t.Name));
                mi.Invoke(instance, new object[] { });
            }
        }
    
        public class Test
        {
            public string ValA { get; set; }
            public int ValB;
            public dynamic ValC;
    
            public void TotoNoParam() { }
            public void Toto(int a) { }
            public string Tata(dynamic b) { return string.Empty; }
        }

    Les résultats :

    Test Appel Direct

    Appel direct x 1        : 00:00:00.0000614

    Appel direct x 10       : 00:00:00.0000003

    Appel direct x 100      : 00:00:00.0000011

    Appel direct x 1000     : 00:00:00.0000076

    Appel direct x 10000    : 00:00:00.0000702

    Appel direct x 100000   : 00:00:00.0007034

    Appel direct x 1000000  : 00:00:00.0072613

    Test Appel Reflection

    Appel Reflection x 1    : 00:00:00.0005199

    Appel Reflection x 10   : 00:00:00.0000188

    Appel Reflection x 100  : 00:00:00.0001620

    Appel Reflection x 1000         : 00:00:00.0016719

    Appel Reflection x 10000        : 00:00:00.0155864

    Appel Reflection x 100000       : 00:00:00.1943837

    Appel Reflection x 1000000      : 00:00:01.8779117

    Test Appel Reflection (optimized)

    Appel Reflection (optimized) x 1        : 00:00:00.0000069

    Appel Reflection (optimized) x 10       : 00:00:00.0000184

    Appel Reflection (optimized) x 100      : 00:00:00.0001509

    Appel Reflection (optimized) x 1000     : 00:00:00.0014998

    Appel Reflection (optimized) x 10000    : 00:00:00.0151252

    Appel Reflection (optimized) x 100000   : 00:00:00.1431094

    Appel Reflection (optimized) x 1000000  : 00:00:01.3252002

    Test Appel Dynamic

    Appel Dynamic x 1       : 00:00:00.0346741

    Appel Dynamic x 10      : 00:00:00.0000053

    Appel Dynamic x 100     : 00:00:00.0000107

    Appel Dynamic x 1000    : 00:00:00.0000921

    Appel Dynamic x 10000   : 00:00:00.0009150

    Appel Dynamic x 100000  : 00:00:00.0088054

    Appel Dynamic x 1000000         : 00:00:00.0764506

    Apparement même en sortant le MethodInfo de l’appel, on obitent encore des résultats plus rapide avec Dynamic qu’avec Reflection.

    Propulsé par WordPress.com.

    %d blogueurs aiment cette page :