Plus....

Beginning Game Development

Partie V – Ajouter des objets

Traduit par Valentin BILLOTTE

Consultez cet article en anglais  

Cet article n’a pas été traduit par Microsoft. Il n’a pas été relu ou vérifié par Microsoft.

Sur cette page

Partie V – Ajouter des objets Partie V – Ajouter des objets
nettoyage de code nettoyage de code
IDispose IDispose
Unités Unités
Genericité Genericité
3D Modeling 3D Modeling
Fichiers X Fichiers X
Mesh Mesh
Mettre à jour la Skybox Mettre à jour la Skybox
Material Material
Charger un Mesh Charger un Mesh
Culling Culling
View Frustum View Frustum
Utiliser le temps pour simuler un mouvement Utiliser le temps pour simuler un mouvement
Niveaux Niveaux
Conclusion Conclusion

Partie V – Ajouter des objets

Bienvenue dans le 5ème article sur l’apprentissage de la programmation de jeu. A ce point de notre apprentissage, nous avons un environnement 3D opérationnel et sommes en mesure de modifier la direction et l’emplacement de la caméra à l’aide d’un clavier et d’une souris. Dans cet article nous allons ajouter des objets 3D sous la forme de fichier mesh à notre jeu et implémenter un culling simplifié.

Avant de commencer, effectuons notre habituel nettoyage de code (en incorporant tous vos retours dont je vous remercie).

Haut de page Haut de page

nettoyage de code

Le nettoyage effectué dans cet article consiste principalement à définir définitivement les touches de navigation et à supprimer les éléments de code qui ne sont plus nécessaires. Ces modifications sont déjà prises en compte dans le code accompagnant cet article.

  • Remplacement de la méthode de conversion radian/degré dans la classe Camera par la fonctionnalité de la classe Geometry.
  • Spécification définitive des assignements de touches dans la méthode CheckForInput de la classe GameEngine nous donnant:
    • W et S déplacent la caméra sur l’axe Z. W pour avancer (positif) et S pour reculer (négatif)
    • A et D déplacent la camera sur l’axe X. A vers la gauche (négatif) et D vers la droite (positif).
    • Q et Z déplacent la camera sur l’axe Y. Q pour monter (positif) et Z pour descendre (négatif). Ce mouvement vertical sera supprimé à terme dans la mesure où les tank ne peuvent pas voler.
  • Ajustement de la position de la camera pour être juste au dessus sur de la surface.
  • Suppression de la classe Joystick.
  • Mise à jour du SDK DirectX à la version d’aout 2006.
  • Modification de la méthode GetElapsedTime de la classe HiResTimer afin qu’elle renvoie un float et changement du type de la variable de la classe GameEngine pour un float.
  • Suppression de HiResTimer.Reset de la méthode Render de la classe GameEngine, déplacement de l’instruction HiResTimer.Start dans le constructeur de la classe GameEngine.
Haut de page Haut de page

IDispose

Vous aurez noté qu’une partie des classes comme Keyboard et Mouse, implémentent l’interface IDisposable. Il s’agit de l’implémentation du template Dispose en .Net. Vous pouvez lire un cours complet sur ce sujet en vous rendant à Implementing Finalize and Dispose to Clean Up Unmanaged Resources.

Le template Dispose en .Net est généralement utilisé lorsqu’un programme utilise des ressources qui ne sont pas managé en .Net. Ces ressources non managés doivent être nettoyées par un moyen spécial pour s’assurer que ce nettoyage se fasse de manière déterministe. Le garbage collector n’étant pas déterministe nous devons suivre un certain nombre d’étapes pour s’assurer que la libération de ces ressources se fasse de manière correcte. Ces étapes sont explicitées dans le tempate Dispose.

Nous utilisons un grand nombre de ressources non managée dans le développement de jeu, il est donc important d’implémenter ce template pour chaque classe qui interagit avec DirectX ou avec des ressources extérieures (comme les fichiers); en gros pour pratiquement chacune nos classes. Nous seront protégés des fuites de mémoires et rendrons notre production plus véloce.

A ce stade du développement, ce qui nous manque dans Battletank 2005 ce sont des unités. Si nous revenons au premier article pour jeter un œil à la capture d’écran du jeu original, nous voyons qu’il nous manque principalement des formes (collines, montagnes, …) et des opposants (sous la forme de tank). Les formes peuvent être des obstacles ou des lieux où se réfugier. Les tanks sont ce que nous allons à priori viser. Ces objets 3D nous aident aussi à nous repérer dans une carte. Nous ajouterons un terrain au prochain article. Concentrons-nous pour l’heure sur l’ajout d’unités.

Haut de page Haut de page

Unités

Nous aurons deux types objets 3D dans BattleTank 2005 : les obstacles et les tanks. La principale différence entre ces deux types d’objets est que les tanks peuvent se déplacer et les obstacles non. A partir du moment où nous allons avoir un grand nombre de tanks et d’obstacles, il est préférable de les gérer en masse plutôt qu’au cas par cas. Nous utiliserons pour cela une collection.

Nous pourrions ajouter ces deux types dans une seule et même collection, mais il est plus logique de leur affecter chacun une collection. Cela nous permettra de nous concentrer sur un groupe plutôt que sur un autre sans avoir à tester le type de chaque unité. Cette méthode nous permet ainsi de mettre a jour la position des tanks sans se soucier des obstacles.

Haut de page Haut de page

Genericité

Les collections dans NET 1.0 and 1.1 étaient des collections d’objets. Il s’agit d’une méthode très flexible qui permet à la collection d’accepter tout et n’importe quoi, mais qui oblige le développeur a «caster» à tout va pour récupérer la classe sous jacente et ses fonctionnalités spécifiques. Cela donne en outre un risque majeur d’exception sur un cast qui échoue.

Avec .Net 2.0 nous pouvons utiliser les collections génériques pour créer des collections spécifiquement faites pour accueillir un type précis d’objets.

Chaque unité possède un part commune sans son architecture et son fonctionnement avec les autres unités du jeu. Afin de rendre le jeu plus simple à maintenir, nous allons factoriser les caractéristiques communes à l’intérieure d’une seule et même classe mère. Toutes les unités vont en hériter et définir leur propre spécificité.

La création de classe mère, de classes dérivées, et d’une hiérarchie objet donne naissance au polymorphisme. Il s’agit là d’une notion très puissante en programmation objet. Reportez vous aux articles suivants de la MSDN :

La classe mère ressemblera à ceci dans notre cas :

Visual C#

public abstract class UnitBase : IDisposable
{ public UnitBase
( Device device, string meshFile, Vector3 position, float scale )
public void Render ( Camera camera )
public bool IsCulled public Vector3 Position
public float Radius private void LoadMesh ( )
private void ComputeRadius ( ) private Vector3 _position;
private float _radius; private bool _isCulled;
private Device _device; private string _meshFile;
private float _scale; private Mesh _mesh = null;
private Material[] _meshMaterials; private Texture[] _meshTextures; }

Visual Basic

Public MustInherit Class UnitBase Implements IDisposable
Public Sub New(ByVal device As Device, ByVal meshFile As String,
ByVal position As Vector3, ByVal scale As Single)
Public Sub Render(ByVal camera As Camera)
Public Property IsCulled() As Boolean Public
Property Position() As Vector3
Public Property X() As Single
Public Property Y() As Single
Public Property Z() As Single
Public ReadOnly Property Radius() As Single
Public Sub Dispose() Implements IDisposable.Dispose
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
Protected Overrides Sub Finalize()
Private _disposed As Boolean Private Sub LoadMesh()
Private Sub ComputeRadius()
Private m_position As Vector3
Private m_radius As Single
Private m_isCulled As Boolean
Private m_device As Device
Private m_meshFile As String
Private m_scale As Single
Private m_mesh As Mesh = Nothing
Private m_meshMaterials As Material()
Private m_meshTextures As Texture() End Class

A ce stade vous devez vous demandez ce que peut bien être un mesh.

Haut de page Haut de page

3D Modeling

Si vous avez vu la création d’une mire dans les articles précédent vous avez du vous dire que la création d’un objet complexe possédant des milliers de vertices dans un fichier de code source C# doit être une opération impossible. Le simple cube de la skybox demande 200 lignes de code. Il y’a heureusement un meilleur moyen pour créer des objets3D.

La majeure partie des objets 3D sont créés à l’aide de programmes spéciaux comme 3ds max ou Maya. Ces programmes sauvegardent les objets 3D créé dans leur propre format propriétaire (iff pour maya et 3ds pour 3ds max). DirectX ne peut pas lire ces formats mais peut lire un autre format nommé le format X.

Haut de page Haut de page

Fichiers X

DirectX définit un format de fichier nommé fichier X. Il contient la définition d’un modèle 3D. L’utilisation de ces fichiers évite aux développeurs des milliers de lignes de code à écrire pour charger un modèle 3D. Un fichier X est aussi nommé un fichier mesh.

Haut de page Haut de page

Mesh

Si vous vous souvenez de notre précédent article, un mesh est un ensemble de données qui décrit un objet 3D. Il contient l’ensemble des vertices du mesh, la façon dont il faut relier ces vertices, et la manière dont la ou les textures associées sont plaquées sur le mesh.

Il existe des utilitaires qui permettent de transformer un fichier représentant un objet 3D dans un format tiers en un fichier au format X. Le SDK DirectX inclus deux de ces utilitaires qui permettent respectivement d’obtenir un fichier X à partir d’un fichier Maya ou 3Ds Max.

Où trouver des fichiers X «clé en main» pour agrémenter notre jeu ? Là encore le SDK inclus un grand nombre de mesh de toutes sortes. Il inclut aussi un utilitaire, le MeshViewer qui permet de visualiser ces fichiers. Reportez vous aux différents répertoires du répertoire d’installation (notamment la répertoire Utilities) pour en savoir plus.

Models 3D gratuits : Ah moins d’être très talentueux, vous aurez sans doute besoin d’une aide extérieure pour obtenir tous les meshes dont vous avez besoin. Internet offre un grand nombre de site donnant accès a des bibliothèques de meshes créé par des artistes dans le but de promouvoir leur talent. Ces meshes peuvent être utilisés dans un but non commercial. Reportez vous aux licences qui les accompagnent pour en savoir plus. Un bon site pour les meshes se trouve à l’adresse: http://www.3dcafe.com.

L’utilisation des fichiers X change radicalement la donne pour l’intégration de modèles 3D dans nos jeux.

Haut de page Haut de page

Mettre à jour la Skybox

La première chose à faire est de nettoyer le code lié à la skybox. Le Sdk inclus un fichier nommé lobby_skybox.x dans le répertoire Samples\Media\Lobby qui décrit un cube identique à celui que nous utilisons. J’ai repris ce fichier X et j’ai changé son nom en skybox.x et modifié le nom des textures pour les faire correspondre aux noms spécifiés dans le fichier X.

Changer les textures : La plupart des fichiers X peuvent être ouvert dans un simple éditeur de texte. Il nous suffit alors de chercher la référence vers les textures à l’intérieur de celui-ci (cherchez la valeur TextureFilename) et de remplacer les noms de textures existantes par celles que nous voulons mettre. Vous pouvez aussi substituer la texture pendant le chargement du mesh à l’aide la méthode TextureLoader.FromFile. Dans la classe Skybox j’ai supprimé la méthode SetupCubeFaces, et les six méthodes commençant par «Copy» (CopyLeftFaceVertexBuffer, CopyFrontFaceVertexBuffer, etc.). J’ai aussi effacé la méthode RenderFace. Dans ce sens vous pouvez supprimer toutes les variables privées déclarée à la fin de la classe à l’exception de la variable Device. Dans la méthode Render, supprimez les lignes de code qui vérifient l’orientation de la caméra. Enfin supprimez l’appel à la méthode SetupCubeFaces dans le constructeur et remplacez là par un appel à LoadMesh.

Ajoutez maintenant les déclarations des trois variables suivantes à la fin de la classe:

La classe mère ressemblera à ceci dans notre cas :

Visual C#

private Mesh _mesh = null;
private Material[] _meshMaterials;
private Texture[] _meshTextures;

Visual Basic

Private m_mesh As Mesh = Nothing
Private m_meshMaterials As Material()
Private m_meshTextures As Texture()

Nous savons déjà ce qu’est un mesh et une texture, mais qu’est ce qu’un Material ?

Haut de page Haut de page

Material

Un Material décrit la manière dont les polygones réfléchissent la lumière. Si les textures donnent un aspect aux polygones, les materials définissent la manière dont ils apparaissent sous la lumière (brillant, matte …).

Haut de page Haut de page

Charger un Mesh

Le chargement d’un mesh à partir d’un fichier est relativement simple. La seule chose à faire est d’appeler la méthode FromFile de la classe Mesh (il nous faudra du temps pour nous familiariser avec cette classe dans la mesure ou elle reste relativement bas niveau.).

Dans la classe Skybox, ajoutez le code suivant:

Visual C#

private void LoadMesh ( )
{ ExtendedMaterial[] materials = null;
Directory.SetCurrentDirectory
( Application.StartupPath + @"\..\..\..\Resources\" );
_mesh = Mesh.FromFile
( @"skybox.x", MeshFlags.SystemMemory, _device, out materials );
if ( ( materials != null ) && ( materials.Length > 0 ) )
  { _meshTextures = new Texture[materials.Length];
  _meshMaterials = new Material[materials.Length];
  for ( int i = 0 ; i &#lt; materials.Length ; i++ )
  { _meshMaterials[i] = materials[i].Material3D;
    _meshMaterials[i].Ambient = _meshMaterials[i].Diffuse;
    if ( materials[i].TextureFilename != null &&
    ( materials[i].TextureFilename != string.Empty ) )
    _meshTextures[i] = TextureLoader.FromFile
    ( _device, materials[i].TextureFilename ); } }
    }

Visual Basic

Private Sub LoadMesh()
Dim materials As ExtendedMaterial() = Nothing
Directory.SetCurrentDirectory(Application.StartupPath + "\..\..\..\Resources\")
m_mesh = Mesh.FromFile("skybox.x", MeshFlags.SystemMemory, m_device, materials)
If (Not (materials Is Nothing)) AndAlso (materials.Length > 0) Then
m_meshTextures = New Texture(materials.Length) {}
m_meshMaterials = New Material(materials.Length) {}
Dim i As Integer = 0
While i &#lt; materials.Length m_meshMaterials(i) = materials(i).Material3D
m_meshMaterials(i).Ambient = m_meshMaterials(i).Diffuse
If Not (materials(i).TextureFilename Is Nothing)
AndAlso (Not (materials(i).TextureFilename = String.Empty)) Then
m_meshTextures(i) = TextureLoader.FromFile(m_device, materials(i).TextureFilename)
End If System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
End While End If End Sub

Bien que DirectX s’occupe de toute la phase de chargement du fichier X, nous devons nous occuper nous même du chargement des materials et des textures.

La dernière étape consiste à modifier la méthode Render de la classe Skybox afin d’intégrer l’affichage de notre skybox. Ajoutez le code suivant juste après l’instruction the _device.RenderState.CullMode = Microsoft.DirectX.Direct3D.Cull.None;

Visual C#

for ( int i = 0 ; i &#lt; _meshMaterials.Length ; i++ )
{ _device.Material = _meshMaterials[i];
_device.SetTexture ( 0, _meshTextures[i] ); _mesh.DrawSubset ( i ); }

Visual Basic

While i &#lt; m_meshMaterials.Length ....m_device.Material = m_meshMaterials(i)
 m_device.SetTexture(0, m_meshTextures(i)) m_mesh.DrawSubset(i)
  System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1) End While

Nous bouclons ici sur les meshMaterials avec un appel à la méthode DrawSubset de la classe Mesh pour afficher tous les subset (partie d’un mesh). Voila! Notre classe Skybox fait maintenant environs 80 lignes de code en plus d’être simple à lire.

Revenons à la classe UnitBase, nous devons nous occuper du flag the IsCulled.

Haut de page Haut de page

Culling

Nous avons brièvement abordé le culling dans notre troisième article. Le culling est la suppression d’objets en entier d’une scène lorsqu’il n’ont pas a être affichés. Dans BattleTank 2005 nous supprimerons tous les objets qui ne se trouvent pas dans le view frustum de la scene.

Afin de savoir si une unité se trouve dans le view frustrum, nous allons améliorer la classe Camera afin de nous fournir toutes les informations sur le afin de vérifier chaque objet et savoir s’il se trouve à l’intérieur ou à l’intérieur du frustum. Nous utiliserons la propriété Radius de la classe UnitBase (calculé dans la méthode ComputeRadius) pour réaliser cette vérification.

La méthode BoundingSphere de la classe Geometry calcule une sphere qui englobe complètement les points d’un mesh en se basant sur ses vertex. Nous devons donc lire le vertex buffer à l’aide d’un lock (afin d’y avoir accès) et en appelant u Unlock à la fin du traitement. Nous utiliserons un bloc Using afin de nettoyer le buffer à la fin de son utilisation.

Visual C#

private void ComputeRadius ( )
{ using ( VertexBuffer vertexBuffer = _mesh.VertexBuffer )
{ GraphicsStream gStream = vertexBuffer.Lock ( 0, 0, LockFlags.None );
  Vector3 tempCenter;
 _radius = Geometry.ComputeBoundingSphere
   ( gStream, _mesh.NumberVertices, _mesh.VertexFormat, out tempCenter ) * _scale;
     vertexBuffer.Unlock ( );
}
}

  

Visual Basic

Private Sub ComputeRadius()
Dim vertexBuffer As VertexBuffer = Nothing
Try vertexBuffer = m_mesh.VertexBuffer
Dim gStream As GraphicsStream = vertexBuffer.Lock(0, 0, LockFlags.None)
Dim tempCenter As Vector3 m_radius = Geometry.ComputeBoundingSphere
(gStream, m_mesh.NumberVertices, m_mesh.VertexFormat,
tempCenter) * m_scale Finally vertexBuffer.Unlock()
vertexBuffer.Dispose() End Try End Sub

Vous devez comprendre que l’utilisation d’une sphère pour englober un objet est une méthode très approximative. Elle est passable pour de petits objets, mais absolument inadaptée pour les gros objets.

Dans les jeux commerciaux, un énorme effort est fournit pour créer une méthode de culling qui élimine le plus d’objets possible le plus rapidement possible. Nous pourrions aussi tenter de déterminer les objets qui ne sont que partiellement visible dans le frustum, mais étant donné notre incapacité à afficher qu’une partie d’un objet c’est une perte de temps. Nous partirons du principe que tous les objets, même s’ils n’ont qu’un point de visible à l’écran sont affichés.

Maintenant que nous avons le radius de notre unité, nous devons étudier le frustum et déterminer si la sphère virtuelle que nous avons créé est en contact avec celui-ci.

Haut de page Haut de page

View Frustum

Ajouter un frustum à une camera est simple. Nous commençons par ajouter des structures de données pour contenir les informations à propos du frustum. Comme nous l’avons dit dans le précédent article, un frustum ressemble à une pyramide dont le sommet a été coupé. Nous devons donc stocker les coins des carres situés à la base et au sommet de notre pyramide ainsi que les plans de chaque cotés. Pour les coins, nous pouvons utiliser une structure de type Vector3. Pour les plans, nous utiliserons plutôt une structure de type Plane.

Visual C#

private Vector3[] _frustumCorners; private Plane[] _frustumPlanes;b

  Visual Basic

private Vector3[] _frustumCorners; private Plane[] _frustumPlanes;b

Nous initialisons le table dans le constructeur (deux carrés de 4 coins donnent 8 points, et les autres cotés du polygone ajoutés à la base et au sommet donnent 6 plans).

Visual C#

_frustumCorners = new Vector3[8]; _frustumPlanes = new Plane[6];

 Visual Basic

m_frustumCorners = New Vector3(8) {} m_frustumPlanes = New Plane(6) {}

Nous devons maintenant calculer le frustum en utilisant le vue courante et la matrice de projection.

Visual C#

private void ComputeViewFrustum ( )
{ Matrix matrix = _viewMatrix * _perspectiveMatrix; matrix.Invert ( );
_frustumCorners[0] = new Vector3 ( -1.0f, -1.0f, 0.0f ); // xyz
_frustumCorners[1] = new Vector3 ( 1.0f, -1.0f, 0.0f ); // Xyz
_frustumCorners[2] = new Vector3 ( -1.0f, 1.0f, 0.0f ); // xYz
_frustumCorners[3] = new Vector3 ( 1.0f, 1.0f, 0.0f ); // XYz
_frustumCorners[4] = new Vector3 ( -1.0f, -1.0f, 1.0f ); // xyZ
_frustumCorners[5] = new Vector3 ( 1.0f, -1.0f, 1.0f ); // XyZ
_frustumCorners[6] = new Vector3 ( -1.0f, 1.0f, 1.0f ); // xYZ
_frustumCorners[7] = new Vector3 ( 1.0f, 1.0f, 1.0f ); // XYZ
for ( int i = 0 ; i &#lt; _frustumCorners.Length ; i++ )
_frustumCorners[i] = Vector3.TransformCoordinate (
_frustumCorners[i], matrix ); // Now calculate the planes
_frustumPlanes[0] = Plane.FromPoints ( _frustumCorners[0],
_frustumCorners[1], _frustumCorners[2] ); // Near
_frustumPlanes[1] = Plane.FromPoints ( _frustumCorners[6],
_frustumCorners[7], _frustumCorners[5] ); // Far
_frustumPlanes[2] = Plane.FromPoints ( _frustumCorners[2],
_frustumCorners[6], _frustumCorners[4] ); // Left
_frustumPlanes[3] = Plane.FromPoints ( _frustumCorners[7],
_frustumCorners[3], _frustumCorners[5] ); // Right
_frustumPlanes[4] = Plane.FromPoints ( _frustumCorners[2],
_frustumCorners[3], _frustumCorners[6] ); // Top
_frustumPlanes[5] = Plane.FromPoints ( _frustumCorners[1],
_frustumCorners[0], _frustumCorners[4] ); // Bottom }

  Visual Basic

Private Sub ComputeViewFrustum()
Dim matrix As Matrix = m_viewMatrix * m_perspectiveMatrix matrix.Invert()
m_frustumCorners(0) = New Vector3(-1.0F, -1.0F, 0.0F)
m_frustumCorners(1) = New Vector3(1.0F, -1.0F, 0.0F)
m_frustumCorners(2) = New Vector3(-1.0F, 1.0F, 0.0F)
m_frustumCorners(3) = New Vector3(1.0F, 1.0F, 0.0F)
m_frustumCorners(4) = New Vector3(-1.0F, -1.0F, 1.0F)
m_frustumCorners(5) = New Vector3(1.0F, -1.0F, 1.0F)
m_frustumCorners(6) = New Vector3(-1.0F, 1.0F, 1.0F)
m_frustumCorners(7) = New Vector3(1.0F, 1.0F, 1.0F)
Dim i As Integer = 0 While i &#lt;
 m_frustumCorners.Length m_frustumCorners(i) =
 Vector3.TransformCoordinate(m_frustumCorners(i), matrix)
  System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1) End While
  m_frustumPlanes(0) = Plane.FromPoints(m_frustumCorners(0),
  m_frustumCorners(1), m_frustumCorners(2))
   m_frustumPlanes(1) = Plane.FromPoints(m_frustumCorners(6),
  m_frustumCorners(7), m_frustumCorners(5))
  m_frustumPlanes(2) = Plane.FromPoints(m_frustumCorners(2),
  m_frustumCorners(6), m_frustumCorners(4))
   m_frustumPlanes(3) = Plane.FromPoints(m_frustumCorners(7),
  m_frustumCorners(3), m_frustumCorners(5))
  m_frustumPlanes(4) = Plane.FromPoints(m_frustumCorners(2),
  m_frustumCorners(3), m_frustumCorners(6))
  m_frustumPlanes(5) = Plane.FromPoints(m_frustumCorners(1),
  m_frustumCorners(0), m_frustumCorners(4))
  End Sub

Nous commençons par combiner les matrices de vue et de projection en les multipliant. Ensuite, nous initialisons les 8 coins du frustum à la manière d’un cube situé juste en face de la caméra. Ces coins sont ensuite transformés pour créé six plans en utilisant la méthode FromPoints de la classe Plane.

Le frustum est calculé à chaque boucle render. Ajoutez un appel à la méthode ComputeViewFrustum à la fin du constructeur de la classe Camera et à la fin de la méthode Render de la classe Camera. Nous pouvons utiliser le frustum et le radius pour chaque unités afin de déterminer si une partie de l’objet est visible dans le frustum et doit être affiché. La méthode IsInViewFrustum renvoie true si l’unité est à l’intérieur du frustum ou false dans le cas contraire.

Visual C#

public bool IsInViewFrustum ( UnitBase unitToCheck )
{ foreach ( Plane plane in _frustumPlanes )
{ if ( plane.A * unitToCheck.Position.X
+ plane.B * unitToCheck.Position.Y
+ plane.C * unitToCheck.Position.Z
+ plane.D &#lt;= ( -unitToCheck.Radius ) )
return false; } return true; }

Nous initialisons le table dans le constructeur (deux carrés de 4 coins donnent 8 points, et les autres cotés du polygone ajoutés à la base et au sommet donnent 6 plans).

Visual Basic

Public Function IsInViewFrustum(ByVal unitToCheck As UnitBase) As Boolean
For Each plane As Plane In m_frustumPlanes
If plane.A * unitToCheck.Position.X
+ plane.B * unitToCheck.Position.Y +
plane.C * unitToCheck.Position.Z
+ plane.D &#lt;= (-unitToCheck.Radius) Then Return False
End If Next Return True End Function

Le processus de culling doit intervenir avant d’afficher l’unité. Nous faisons cela dans la méthode Render de la classe BaseUnit en vérifiant le frustum de la caméra avant l’affichage du mesh.

Visual C#

if ( camera.IsInViewFrustum ( this ) == false ) return;

Nous initialisons le table dans le constructeur (deux carrés de 4 coins donnent 8 points, et les autres cotés du polygone ajoutés à la base et au sommet donnent 6 plans).

Visual Basic

If camera.IsInViewFrustum(Me) = False Then Return End If

Maintenant que notre infrastructure de base est en place, nous pouvons commencer à ajouter des unîtes. La classe UnitBase est abstraire, elle ne peut donc pas être instanciée. Nous devons donc créer des classes filles qui vont utiliser les fonctionnalités de leur classe mère et en ajouter de spécifique. La première classe à créer pour BattleTank 2005 est bien entendu la classe Tank et Obstacle.

Visual C#

public class Obstacle :
UnitBase { public Obstacle ( Device device,
string meshFile, Vector3 position, float scale ) :
base ( device, meshFile, position, scale ) { } }

Visual Basic

Public Class Obstacle Inherits UnitBase
Public Sub New(ByVal device As Device, ByVal meshFile As String,
ByVal position As Vector3, ByVal scale As Single)
MyBase.New(device, meshFile, position, scale) End Sub End Class

La classe Obstacle ne fait rien d’autre que de se référer à sa mère. Nous lui ajouterons des spécificités plus tard.

Visual C#

public class Tank : UnitBase { public Tank
( Device device, string meshFile,
Vector3 position, float scale ) :
base ( device, meshFile, position, scale )
public void Update ( float deltaTime )
private float _speed = 10.0f; }

Visual Basic

Public Class Tank Inherits UnitBase
Public Sub New(ByVal device As Device,
ByVal meshFile As String, ByVal position As Vector3,
ByVal scale As Single)
MyBase.New(device, meshFile, position, scale)
End Sub
Public Sub Update(ByVal deltaTime As Single)
MyBase.Z -= (m_speed * deltaTime) End Sub
Private m_speed As Single = 10.0F End Class

La classe Tank ajoute une propriété _speed sur laquelle nous reviendrons et une méthode Update.

Haut de page Haut de page

Utiliser le temps pour simuler un mouvement

La méthode Update prend en paramètre un float qui représente le temps passé en seconde depuis la dernière boucle de jeu. Dans le second article, nous avons ajouté une variable deltaTime que nous avons utilisé pour calculer le FPS de notre jeu. C’est cette valeur que nous allons utiliser désormais pour calculer la position des objets qui se déplacent. Utiliser le temps pour calculer la vitesse des objets permet de s’assurer que quelque soit le pc sur lequel s’exécute le jeu le mouvement apparaisse fluide et identique quelque soit la vitesse de la machine. Si nous déplacions notre objet d’une unité à chaque boucle de jeu, il se déplacera terriblement vite sur les machines rapides et lentement sur les vieux PC. Vous pourriez tester cela avec le cube rotatif que nous affichions jusqu’alors. Nous pourrions forcer le même nombre de boucle par secondes. Là encore une boucle ne s’exécute pas forcement à la même vitesse qu’une autre boucle suivant le travail du CPU en cours.

Visual C#

foreach ( Tank tank in _tanks ) { tank.Update ( _deltaTime ); }

Visual Basic

For Each tank As Tank In m_tanks tank.Update(m_deltaTime) Next

La méthode Update déplace simplement le tank vers l’origine en utilisant une vitesse prédéfinie.

Visual C#

public void Update ( float deltaTime ) { base.Z -= ( _speed * deltaTime ); }

Visual Basic

Public Sub Update(ByVal deltaTime As Single)
MyBase.Z -= (m_speed * deltaTime) End Sub

Nous créons maintenant deux collections génériques dans la classe GameEngine afin de stocker les unités mobiles et figées.

Visual C#

private List&#lt;UnitBase&#gt;
_obstacles; private List&#lt;UnitBase&#gt; _tanks;

Visual Basic

private List&#lt;UnitBase&#gt; _obstacles;
private List&#lt;UnitBase&#gt; _tanks;

Les unîtes sont ajoutés à ces collections dans les méthodes CreateObstacles et CreateTanks.

Visual C#

private void CreateObstacles ( )
{ _obstacles = new List&#lt;UnitBase&#gt; ( ); _obstacles.Add
( new Obstacle ( _device, @"car.x", new Vector3 ( 0, 0, 200 ), 1f ) );
_obstacles.Add (
new Obstacle ( _device, @"car.x", new Vector3 ( 60, 0, 100 ), 1f ) );
_obstacles.Add (
new Obstacle ( _device, @"car.x", new Vector3 ( -60, 0, 150 ), 1f ) );
_obstacles.Add (
new Obstacle ( _device, @"car.x", new Vector3 ( 60, 0, -100 ), 1f ) );
_obstacles.Add (
new Obstacle ( _device, @"car.x", new Vector3 ( -60, 0, -150 ), 1f ) ); }
private void CreateTanks ( )
{ _tanks = new List&#lt;UnitBase&#gt; ( );
_tanks.Add (
new Tank ( _device, @"bigship1.x", new Vector3 ( 0, 0, 200 ), 1f ) );
_tanks.Add (
new Tank ( _device, @"bigship1.x", new Vector3 ( 100, 0, 300 ), 1f ) );
_tanks.Add (
new Tank ( _device, @"bigship1.x", new Vector3 ( -100, 0, 500 ), 1f ) );
_tanks.Add (
new Tank ( _device, @"bigship1.x", new Vector3 ( 100, 0, -200 ), 1f ) );
_tanks.Add (
new Tank ( _device, @"bigship1.x", new Vector3 ( -100, 0, -400 ), 1f ) ); }

Visual Basic

Private Sub CreateObstacles() m_obstacles = New List(Of UnitBase)()
m_obstacles.Add(
New Obstacle(m_device, "car.x", New Vector3(0, 0, 200), 1.0F))
m_obstacles.Add(
New Obstacle(m_device, "car.x", New Vector3(60, 0, 100), 1.0F))
m_obstacles.Add(
New Obstacle(m_device, "car.x", New Vector3(-60, 0, 150), 1.0F))
m_obstacles.Add(
New Obstacle(m_device, "car.x", New Vector3(60, 0, -100), 1.0F))
m_obstacles.Add(
New Obstacle(m_device, "car.x", New Vector3(-60, 0, -150), 1.0F)) End Sub
Private Sub CreateTanks()
m_tanks = New List(Of UnitBase)
m_tanks.Add(
New Tank(m_device, "bigship1.x", New Vector3(0, 0, 200), 1.0F))
m_tanks.Add(
New Tank(m_device, "bigship1.x", New Vector3(100, 0, 300), 1.0F))
m_tanks.Add(
New Tank(m_device, "bigship1.x", New Vector3(-100, 0, 500), 1.0F))
m_tanks.Add(
New Tank(m_device, "bigship1.x", New Vector3(100, 0, -200), 1.0F))
m_tanks.Add(
New Tank(m_device, "bigship1.x", New Vector3(-100, 0, -400), 1.0F)) End Sub

Si vous jetez un œil aux coordonnées des obstacles et des tanks, vous verrez que je les place le long des axes. Vous pouvez modifier ces méthodes pour créer des obstacles et tanks à des positons aléatoires. Faites attention toutefois à éviter que les objets ne se chevauchent où soit placés trop prés de la caméra.

Haut de page Haut de page

Niveaux

Un meilleur moyen pourrait être de stocker les positions des obstacles et des tanks dans un fichier. Celui-ci pourrait contenir d’autre données relatives au terrain et chargé automatiquement. Nous serions en mesure de créer des niveaux prédéfinis sous la forme de scénarios. Ces niveaux seraient progressifs et permettraient au joueur d’avoir une envie de jouer sans cesse renouvelée. Ceci ne peut être fait avec un placement aléatoire. Avec un peu d’imagination nous pouvons même penser à créer un éditeur de monde…

La dernière étape consiste tout simplement à appeler ces méthodes. Le meilleur emplacement pour cela est la constructeur de la classe GameEngine juste après la création de notre objet de type Camera.

Visual C#

public GameEngine ( ) { InitializeComponent ( );
this.SetStyle (
ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true );
ConfigureInputDevices ( );
ConfigureDevice ( );
_skyBox = new SkyBox ( this._device );
_camera = new Camera ( ); CreateObstacles ( ); CreateTanks ( );
this.Size = new Size ( 800, 600 ); HiResTimer.Start ( ); }

Visual Basic

Public Sub New() InitializeComponent()
Me.SetStyle(
ControlStyles.AllPaintingInWmPaint Or ControlStyles.Opaque, True)
ConfigureInputDevices()
ConfigureDevice() m_skyBox = New SkyBox(Me.m_device)
m_camera = New Camera CreateObstacles()
CreateTanks() Me.Size = New Size(800, 600)
HiResTimer.Start() End Sub

A chaque boucle render, nous devons itérer sur chaque collection et déterminer quelles unités sont visibles dans le frustum. Nous appelons la méthode RenderUnits dans la méthode Render de la classe GameEngine juste après l’affichage de la skybox.

Visual C#

private void RenderUnits ( ) {
foreach ( UnitBase ub in _obstacles ) { ub.Render ( _camera ); }
foreach ( UnitBase ub in _tanks ) { ub.Render ( _camera ); } }

Visual Basic

Private Sub RenderUnits()
For Each ub As UnitBase In m_obstacles ub.Render(m_camera) Next
For Each ub As UnitBase In m_tanks ub.Render(m_camera) Next End Sub

Terminé. Nous avons maintenant des unités dans notre jeu.

Haut de page Haut de page

Conclusion

Si vous lancez le jeu, vous remarquerez trois choses :

  • Les unités semblent flotter dans l’espace.
  • Les unités sont blanches.
  • Vous pouvez passer à travers les unités.

Le premier problème peut être résolu en ajoutant un terrain au jeu afin afficher une surface solide. Le second problème peut être résolu en ajoutant de l’éclairage à la scène. Le dernier problème sera résolu une fois que nous aurons abordé la détection de collisions. Malgré ces problèmes, notre jeu devient de plus en plus jouable au fil des articles. Dans le prochain article nous verrons comment ajouter un terrain en utilisant un heightmap, comment ajouter des lumières et voir la couleur réelle des objets et enfin nous verrons la détection de collision.

Comme vous avez du le remarquer, j’ai modifié (et je continuerai dans ce sens) les différents graphismes du jeu. Ceci dans le but de vous encourager à ajouter vos propres graphismes et changer l’aspect du jeu de manière radicale.

J’ai manqué de temps encore une fois. J’avais promis de rajouter la mire. Je vais tenter de faire cela dans le prochain article. J’avais promit aussi de revenir sur l’action mapping afin d’avoir une gestion des entrées clavier et souris plus intelligente. Ce sujet devra être couvert dans un futur article.

D’ici là, joyeux développements!

Haut de page Haut de page Précédent 5 sur 7 Suivant