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 objetsBienvenue 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).
nettoyage de codeLe 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.
IDisposeVous 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.
UnitésNous 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.
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 Visual Basic Public MustInherit Class UnitBase Implements IDisposable A ce stade vous devez vous demandez ce que peut bien être un mesh.
3D ModelingSi 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.
Fichiers XDirectX 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.
MeshSi 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.
Mettre à jour la SkyboxLa 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; Visual Basic Private m_mesh As Mesh = Nothing Nous savons déjà ce qu’est un mesh et une texture, mais qu’est ce qu’un Material ?
MaterialUn 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 …).
Charger un MeshLe 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 ( ) Visual Basic Private Sub LoadMesh() 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++ ) Visual Basic While i &#lt; m_meshMaterials.Length ....m_device.Material = m_meshMaterials(i) 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.
CullingNous 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 ( )
Visual Basic Private Sub ComputeRadius() 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.
View FrustumAjouter 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 ( ) Visual Basic Private Sub ComputeViewFrustum() 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 ) 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 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 : Visual Basic Public Class Obstacle Inherits UnitBase 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 Visual Basic Public Class Tank Inherits UnitBase La classe Tank ajoute une propriété _speed sur laquelle nous reviendrons et une méthode Update.
Utiliser le temps pour simuler un mouvementLa 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) 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; Visual Basic private List&#lt;UnitBase&#gt; _obstacles; Les unîtes sont ajoutés à ces collections dans les méthodes CreateObstacles et CreateTanks. Visual C# private void CreateObstacles ( ) Visual Basic Private Sub CreateObstacles() m_obstacles = New List(Of UnitBase)() 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.
NiveauxUn 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 ( ); Visual Basic Public Sub New() InitializeComponent() 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 ( ) { Visual Basic Private Sub RenderUnits() Terminé. Nous avons maintenant des unités dans notre jeu.
ConclusionSi vous lancez le jeu, vous remarquerez trois choses :
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!
|