XNA Tutorial 7 : Cas concret, l'affichage d'une ville et du système solaire

Retourner au sommaire des cours  

Nous avons passé, avec le dernier tutorial, une première étape dans le développement de jeux. Ce tutorial marque la fin de cette étape et, en conclusion, va nous apprendre à utiliser nos connaissances acquises pour créer une ville (rudimentaire, il faut rester humble), et pour créer une simulation de notre bon vieux système solaire. Ces deux projets ne sont pas seulement ludiques, il vont nous être très utiles. La ville tout d'abord va nous permettre de bien comprendre comment s'orienter dans l'espace et de mesurer l'utilité d'un bon système objet. La simulation quand à elle, poussera nos connaissances et notre maitrise des Matrices dans leur derniers retranchements. Ces deux projets seront aussi la base de nombreux projets qui vont suivre pour expliciter les notions que nous apprendrons aux fils des tutoriaux qui vont suivre.

L'objet de toutes les convoitises

La ville que nous allons créer sera simple. Chaque paté de maison sera en fait constitué d'un seul batiment dont la hauteur variera de manière aléatoire. Nous utiliserons pour la créer trois types de cubes : un cube pour créer le sol, un cube par paté pour créer les trottoires, un cube pour créer chaque gratte ciel. Evidemment nous n'allons pas comme dans le précédent tutoriel, nous amuser à instancier un vertex buffer et un index buffer pour chaque cube créé à la main. Nous allons plutot utiliser une classe "Cube" qui va faire tout le travail à notre place en nous offrant deux méthodes : Load et Render. Nous reprendrons le code du tutorial précédent en le factorisant pour permettre un développement plus modularisé.

La nouvelle classe Cube se présente ainsi :

/// <summary>/// <para>3D representation of a cube.</para>/// </summary>public class Cube : IDisposable{     #region Private members     private GraphicsDevice _device;    private VertexBuffer _vertexBuffer = null;    private IndexBuffer _indexBuffer = null;    private float _width;    private float _height;    private float _depth;    private Color _color = Color.TransparentWhite;    private float _x, _y, _z;    private float _rotationX, _rotationY, _rotationZ;    private static short[] indices = new short[36]{0,1,2,                                          0,2,3,                                          3,2,4,                                          3,4,5,                                          5,4,7,                                          5,7,6,                                          6,7,1,                                          6,1,0,                                          6,0,3,                                          6,3,5,                                          1,7,4,                                          1,4,2};    private Matrix _transformationMatrix;    private Matrix _translationMatrix;    private Matrix _scaleMatrix;    private Matrix _rotationMatrix;     #endregion      #region Properties      /// <summary>    /// <para>Gets or sets the Cube's color.</para>    /// </summary>    public Color Color    {        get        {            return this._color;        }        set        {            this._color = value;        }    }     /// <summary>    /// <para>Gets or sets the Cube's current transformation.</para>    /// </summary>    public Matrix Transformation    {        get        {            return this._transformationMatrix;        }        set        {            this._transformationMatrix = value;        }    }     /// <summary>    /// <para>Gets the cube's Width.</para>    /// </summary>    public float Width    {        get        {            return this._width;        }    }     /// <summary>    /// <para>Gets the cube's height.</para>    /// </summary>    public float Height    {        get        {            return this._height;        }    }     /// <summary>    /// <para>Gets the cube's depth.</para>    /// </summary>    public float Depth    {        get        {            return this._depth;        }    }     /// <summary>    /// <para>Gets the cube's position on x-axis.</para>    /// </summary>    public float X    {        get        {            return this._x;        }    }     /// <summary>    /// <para>Gets the cube's position on y-axis.</para>    /// </summary>    public float Y    {        get        {            return this._y;        }    }     /// <summary>    /// <para>Gets the cube's position on z-axis.</para>    /// </summary>    public float Z    {        get        {            return this._z;        }    }     #endregion      #region Constructors     /// <summary>    /// <para>Instanciate a new Cube objet.</para>    /// </summary>    public Cube()    {        this._transformationMatrix = Matrix.Identity;        this._vertexBuffer = null;         this._width = 3.0f;        this._height = 3.0f;        this._depth = 3.0f;         this._x = 50.0f;        this._y = 10.0f;        this._z = 10.0f;         this._rotationMatrix = Matrix.Identity;        this._translationMatrix = Matrix.Identity;        this._scaleMatrix = Matrix.CreateScale(1f, 1f, 1f);    }     public void Dispose()    {        if (this._vertexBuffer != null)            this._vertexBuffer.Dispose();         if (this._indexBuffer != null)            this._indexBuffer.Dispose();         this._indexBuffer = null;        this._vertexBuffer = null;    }      #endregion      #region Initialization     public void Load(GraphicsDevice device)    {        this._device = device;        this.InitializeIndices();        this.InitializeVertices();    }     private void InitializeIndices()    {        this._indexBuffer = new IndexBuffer(            this._device,            typeof(short),            36,            ResourceUsage.WriteOnly,            ResourceManagementMode.Automatic);         this._indexBuffer.SetData(Cube.indices);    }     private void InitializeVertices()    {        VertexPositionColor[] vertices = new VertexPositionColorMusic;         vertices[0].Position = new Vector3(0f, 0f, 0f);        vertices[0].Color = this.Color;        vertices[1].Position = new Vector3(0f, 0f, 1f);        vertices[1].Color = this.Color;        vertices[2].Position = new Vector3(1f, 0f, 1f);        vertices[2].Color = this.Color;        vertices[3].Position = new Vector3(1f, 0f, 0f);        vertices[3].Color = this.Color;        vertices[4].Position = new Vector3(1f, 1f, 1f);        vertices[4].Color = this.Color;        vertices[5].Position = new Vector3(1f, 1f, 0f);        vertices[5].Color = this.Color;        verticesDevil.Position = new Vector3(0f, 1f, 0f);//        verticesDevil.Color = this.Color;        vertices[7].Position = new Vector3(0f, 1f, 1f);        vertices[7].Color = this.Color;         this._vertexBuffer = new VertexBuffer(        this._device,        typeof(VertexPositionColor),        8,        ResourceUsage.WriteOnly,        ResourceManagementMode.Automatic);         this._vertexBuffer.SetData(vertices);    }      #endregion      #region Rendering     /// <summary>    /// <para>Render the cube on the device.</para>    /// </summary>    public void Render()    {        this._device.Vertices[0].SetSource(this._vertexBuffer, 0, VertexPositionColor.SizeInBytes);        this._device.Indices = this._indexBuffer;        this._device.VertexDeclaration = new VertexDeclaration(this._device, VertexPositionColor.VertexElements);        this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, 12);     }       #endregion      #region Public methods     /// <summary>    /// Sets a size for the cube.    /// </summary>    public void SetRotation(float rotationX, float rotationY, float rotationZ)    {        this._rotationX = rotationX;        this._rotationY = rotationY;        this._rotationZ = rotationZ;         this._rotationMatrix = Matrix.CreateRotationX(rotationX) * Matrix.CreateRotationY(rotationY) * Matrix.CreateRotationZ(rotationZ);        this.UpdateTransformation();    }     /// <summary>    /// Sets a size for the cube.    /// </summary>    public void SetSize(Vector3 size)    {        this._width = size.X;        this._height = size.Z;        this._depth = size.Y;         this._scaleMatrix = Matrix.CreateScale(size.X, size.Y, size.Z);        this.UpdateTransformation();    }     /// <summary>    /// Sets a position for the cube.    /// </summary>    public void SetPosition(Vector3 location)    {        this._x = location.X;        this._y = location.Y;        this._z = location.Z;         this._translationMatrix = Matrix.CreateTranslation(location.X, location.Y, location.Z);        this.UpdateTransformation();    }     private void UpdateTransformation()    {        this._transformationMatrix = this._scaleMatrix * this._rotationMatrix * this._translationMatrix;    }     #endregion }
 

rien de bien compliqué ; il s'agit d'une simple factorisation du code du tutoriel 6. J'ai pris le code de la classe Game1 d'alors pour le copier coller à l'intérieur de cette classe. Si vous regardez le code de la nouvelle classe Game1 vous  verrez une nette amélioration de la lisibilité du code. Notons toutefois que nous utilisons ici une couleur qui peut être spécifiée lors de la création du cube en lieu et place des huit couleurs que nous utilisions jusqu'ici pour chaque sommet du cube.

Le constructeur de Game1 contient désormais une nouvelle instruction :

this._cube = new Cube();

La méthode Initialize ne fait plus appel à this.InitializeIndices();this.InitializeVertices();

mais à

this.InitializeCubes();

qui correspond à la méthode

private void InitializeCubes()
{
    this._cube.Load(this.graphics.GraphicsDevice);

}

La méthode d'affichage dessine le cube en une instruction :

this._cube.Render();

Enfin, les actions utilisateur sur le clavier modifient la taille et la position du code par l'intermédiaire d'appels à des méthodes explicites :

this._cube.SetSize(size);

this._cube.SetPosition(position);

La transformation étant obtenue par l'intermédiaire de la propriété Transformation. Celle-ci se calcule automatiquement à partir des méthodes SetRotation, SetSize et SetPosition.

//modulolong iTime = (long)(gameTime.TotalGameTime.TotalMilliseconds % 2000f);//passage en radianfloat fAngle = iTime * (2.0f * MathHelper.Pi) / 2000.0f; this._cube.SetSize(size);this._cube.SetPosition(position);             //la transformatio en elle mêmeMatrix world = Matrix.CreateRotationY(fAngle)    * Matrix.CreateRotationX(fAngle)    * this._cube.Transformation; 

effect.Parameters["xWorld"].SetValue(world);

A l'exécution vous obtenez pourtant une application dont le rendu est complètement différent de notre précédent tutoriel :

En fait on peut avoir l'impression au premier abord d'avoir regressé. Au contraire nous avons fait un grand pas en avant ! Notre cube est plus petit tout simplement parcequ'il possède désormais une taille unitaire (1 pour chacun de ses arrètes contre 2 précédemment), afin de facilement spécifier des tailles précises. Il possède une couleur pâle. Ceci est du au fait que nous affectons à chaque vertex la couleur spécifiée par l'utilisateur avant l'appel à la méthode Load du cube. Reportez vous au code de la méthode InitializeVertices pour mieux comprendre :

private void InitializeVertices(){    VertexPositionColor[] vertices = new VertexPositionColorMusic;     vertices[0].Position = new Vector3(0f, 0f, 0f);    vertices[0].Color = this.Color;    vertices[1].Position = new Vector3(0f, 0f, 1f);    vertices[1].Color = this.Color;    vertices[2].Position = new Vector3(1f, 0f, 1f);    vertices[2].Color = this.Color;    vertices[3].Position = new Vector3(1f, 0f, 0f);    vertices[3].Color = this.Color;    vertices[4].Position = new Vector3(1f, 1f, 1f);    vertices[4].Color = this.Color;    vertices[5].Position = new Vector3(1f, 1f, 0f);    vertices[5].Color = this.Color;    verticesDevil.Position = new Vector3(0f, 1f, 0f);//    verticesDevil.Color = this.Color;    vertices[7].Position = new Vector3(0f, 1f, 1f);    vertices[7].Color = this.Color;     this._vertexBuffer = new VertexBuffer(    this._device,    typeof(VertexPositionColor),    8,    ResourceUsage.WriteOnly,    ResourceManagementMode.Automatic);     this._vertexBuffer.SetData(vertices);

}

Enfin pour terminer, le cube ne semble pas tourner sur lui-même mais par rapport à un point correspondant au vertice 0. Là encore tout est normal, le vertice 0 se trouve à l'origine dans le repère 3D à l'intérieur duquel nous créer notre cube (0f, 0f, 0f). C'est par rapport à l'origine d'un objet 3D que se calculent toutes les transformations qui lui sont appliquées. Notre cube se voit rotaté sur lui-même, cette rotation s'effectuera donc par rapport à ce vertice.

Pour terminer  ce point il est plus que necessaire de lire l'article qui se trouve ici, pour bien comprendre l'importance de l'ordre de la multiplication des matrices de transformation (rotation, redimentionnement, translation) pour modifier l'affichage d'un objet à l'écran afin de vous éviter toute surprise et bug de rendu incompréhensible.

La théorie étant acquise, un exercice peaufinera notre pratique : Maintenant que nous avons une classe clé en main pour créer un cube et l'afficher simplement, essayez d'en afficher plusieurs à l'écran.

 

telecharger Vous pouvez télécharger le sample, les deux moteurs et les exercices ici.   

 

New York !

Si vous avez reussi l'exercice précédent, notre premier monde de jeu sera une formalité pour vous. La ville que nous allons créer ici sera plus que rudimentaire. Le sol sera un cube avec une hauteur de 1, les trottoires des pavés applatis avec une hauteur de 5, enfin les batiments seront eux aussi des cubes dont la hauteur sera variable. A ce stade de nos connaissances nous ne pouvons pas ajouter de lumières d'ambiance, pas de texture aux immeubles et pas d'optimisation d'affichage. C'est pourtant un avantage certain puisque dans les articles qui suivront nous améliorerons le code dans ce sens et pourrons mesurer facilement l'importances des notions que nous allons acquerir. Cette ville nous offre aussi la possibilité de bien comprendre comment placer ses objets à l'écran.

Les seules modifications que nous apporterons au programme que nous venons de faire vont porter sur la classe Game1. Nous venons d'énumérer trois types de cube. Commencez donc à déclarer ces cubes au tout début de cette classe :

private Cube _ground;
private Cube[] _trottoires;
private Cube[] _batiments;

 Ajoutez de même un ensemble de constantes qui nous éviterons de remplir notre code de valeur numériques incompréhensibles :

private static int NumberOfBatimentsOnASide = 5;
private static int BatimentSize = 50;
private static int TrottoireSize = 70;
private static int TrottoireHeight = 5;
private static int RoadWidth = 40;
private static int BatimentMinimalHeight = 50;

private static int BatimentMaximalHeight = 450;

Celles ci sont assez explicites pour ne pas avoir à être présentées. Le constructeur instanciera tous les cubes.

_ground = new Cube();
_trottoires = new Cube[NumberOfBatimentsOnASide * NumberOfBatimentsOnASide];
_batiments = new Cube[NumberOfBatimentsOnASide * NumberOfBatimentsOnASide];


Et la méthode InitializeCubes les chargera en mémoire


 

private void InitializeCubes(){    TerrainSize = (RoadWidth + TrottoireSize) * NumberOfBatimentsOnASide + RoadWidth;    Random random = new Random();     //sol    this._ground.Color = Color.Gray;    this._ground.Load(this.graphics.GraphicsDevice);    this._ground.SetSize(new Vector3(        TerrainSize,        TerrainSize,        1));    this._ground.SetPosition(new Vector3(-TerrainSize / 2, -TerrainSize / 2, 0));     //trottoires    for (int i = 0; i < NumberOfBatimentsOnASide; i++)    {        for (int j = 0; j < NumberOfBatimentsOnASide; j++)        {            this._trottoires[i * NumberOfBatimentsOnASide + j] = new Cube();             this._trottoires[i * NumberOfBatimentsOnASide + j].Color = Color.LightGray;            this._trottoires[i * NumberOfBatimentsOnASide + j].Load(this.graphics.GraphicsDevice);            this._trottoires[i * NumberOfBatimentsOnASide + j].SetSize(new Vector3(TrottoireSize, TrottoireSize, TrottoireHeight));             _trottoires[i * NumberOfBatimentsOnASide + j].SetPosition(new Vector3(RoadWidth + j * (TrottoireSize + RoadWidth) - TerrainSize/2 , RoadWidth+(TrottoireSize-BatimentSize)/2 + BatimentSize + i * (TrottoireSize + RoadWidth) - TerrainSize/2 , 0));        }    }     //grattes ciels     for (int i = 0; i < NumberOfBatimentsOnASide; i++)    {        for (int j = 0; j < NumberOfBatimentsOnASide; j++)        {            this._batiments[i * NumberOfBatimentsOnASide + j] = new Cube();