Astuces DotNet (Sébastien Courtois)

26/11/2010

[DirectX 10/11] Tutoriel 2 : Création d’un device Direct3D

Filed under: 3D, C++, Débutant, DirectX, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 17:26

Après un premier article d’introduction à DirectX, nous allons maintenant voir comment créer un device Direct3D (accès à la carte graphique). L’idée générale de cette série d’article est de réaliser un moteur de jeu gérant à la fois DirectX 10 et 11 afin de voir les évolutions des deux technologies.

  • Architecture générale du moteur

Avant de rentrer dans le vif du sujet, nous allons parler un peu architecture. Afin de simplifier la gestion des différentes versions de DirectX, nous allons créer une classe pour gérer chacune des versions de DirectX. Ces classes hériteront d’une super classe indiquant les méthodes obligatoires pour un gestionnaire.

dxt2-archi1

Les trois méthodes de DxManager sont :

  • Init(HWND* hWnd) : Initialisation du gestionnaire DirectX. Cette méthode va créer l’ensemble des ressources nécessaire pour l’utilisation de DirectX. Cette méthode ne sera appelé qu’une seule fois lors du lancement du programme. Il est nécessaire de lui fournir le handle de la fenêtre afin que DirectX puisse savoir où il doit afficher le rendu. Cette méthode renvoie ‘false’ en cas de problème lors de l’initialisation.
  • Render() : Méthode de rendu. Cette méthode sera appelée à chaque fois que l’on souhaite dessiner quelque chose à l’écran.
  • Error(LPCST msg) : Gestion des erreurs. Affichage de messages d’erreurs (MessageBox).

C’est le WinMain de l’application qui sera chargé de créer le gestionnaire directX approprié.

//DirectX 10/11 Manager
DXManager *dxManager;
//Activation/Désactivation de DirectX 11
bool DirectX11Enabled = true;

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hInstance,nCmdShow)) return 0;

    if(DirectX11Enabled)
        dxManager = new DX11Manager();
    else
        dxManager = new DX10Manager();

    if(!dxManager->Init(&hWnd)) return -1;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
        dxManager->Render();
    }
    return (int) msg.wParam;
}

  • Informations générales sur l’initialisation Direct3D

Dans ce tutorial, nous allons principalement parler de l’initialisation de Direct3D. Celle-ci se passe en 5 étapes :

  1. Création de la SwapChain
  2. Création du Device
  3. Récupération du Backbuffer
  4. Création du RenderTarget
  5. Création du ViewPort

L’étape la plus importante est la création du Device (Etape 2). Le Device est l’accès à la carte graphique. C’est à travers cet objet que nous allons pouvoir fournir les informations 3D que la carte graphique devra afficher.

Une autre notion importante est la notion de FrontBuffer et de BackBuffer. La carte graphique contient une zone mémoire où se trouve les données pour les pixels affichés à l’écran (FrontBuffer). Si l’on modifie l’un de ces pixels directement dans le FrontBuffer, le résultat sera instantanément affiché à l’écran. Le problème est que, si la chose que l’on souhaite dessiner à l’écran prend un peu trop de temps, on verra les morceaux du dessins apparaitre petit à petit ce qui peut être désagréable.Pour éviter ce problème, on utilise une deuxième zone mémoire nommé ‘BackBuffer’ qui permet de réaliser le dessin complet puis, une fois fini, d’envoyer l’ensemble des données dans le FrontBuffer pour affichage. Pour résumer, un buffer pour dessiner et un buffer pour afficher. Ce processus “d’échange” de buffer est réalisé par un élément de DirectX nommé SwapChain (Swap = échange en anglais).

IC412615

Fonctionnement de la SwapChain (tiré de MSDN). Comme vous pouvez le voir, il est possible d’avoir plusieurs BackBuffer.

Le RenderTarget défini l’endroit où l’on souhaite dessiner les données envoyés à la carte graphique. Dans notre cas, on souhaite dessiner sur le BackBuffer. Le renderTarget sera donc lié à notre BackBuffer.

Le Viewport défini la zone de l’écran sur laquelle on souhaite dessiner les pixels. On utilisera généralement l’ensemble de la fenêtre. Mais, dans certains cas, on peut souhaite afficher des choses différentes selon les parties de la fenêtre.

super_mario_kart-155809-1

Exemple d’utilisation de multiple Viewports avec un Viewport par joueur (Mario Kart SNES).

  • Gestionnaire Direct3D 10

Le gestionnaire DirectX 10 est constitué d’un fichier DX10Manager.h contenant la définition de la classe et une classe DX10Manager.cpp contenant son implémentation.

  1. Définition de la classe DX10Manager
#pragma once
#include <Windows.h>
#include <D3DX10.h>
#include "DXManager.h"

class DX10Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D10Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D10RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D10_VIEWPORT                ViewPort;

public:
    DX10Manager(void);
    //Destructeur
    ~DX10Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11 Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

2.     Implémentation de la classe DX10Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX

bool DX10Manager::Init(HWND* hWnd)

{

    //Sauvegarde du handle de la fenêtre

  
this->hWnd = hWnd;

   
    //Récupération des dimensions de la fenêtre

  
RECT rc;

    GetClientRect( *hWnd, &rc );

    UINT width = rc.right – rc.left;

    UINT height = rc.bottom – rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

    //Création de la Swap Chain
    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));
    
    //Création de deux buffers (un back buffer et un front buffer)
    swapChainDesc.BufferCount = 2;
    swapChainDesc.BufferDesc.Width = width;
    swapChainDesc.BufferDesc.Height = height;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    //Définition du nombre d'image par secondes maximun (60 images).
    swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
    swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    
    //Multisampling setting
    swapChainDesc.SampleDesc.Quality = 0;
    swapChainDesc.SampleDesc.Count = 1;

    //Liaison de la swapchain et de le fenêtre
    swapChainDesc.OutputWindow = *hWnd;
    swapChainDesc.Windowed = true;    

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
HRESULT codeErreur = D3D10CreateDeviceAndSwapChain( NULL, 
                                                    D3D10_DRIVER_TYPE_HARDWARE, 
                                                    NULL, 
                                                    D3D10_CREATE_DEVICE_DEBUG, 
                                                    D3D10_SDK_VERSION, 
                                                    &swapChainDesc, 
                                                    &this->SwapChain, 
                                                    &this->D3DDevice); 
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D10CreateDeviceAndSwapChain. Le premier paramètre représente l’adapteur à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D10_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D10_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini le numéro de version du SDK DirectX utilisé pour créer le programme.

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le sixième paramètre fourni la structure de description de cette SwapChain.

Les deux derniers paramètres sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

La méthode D3D10CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

    //Création du rendertargetview
    ID3D10Texture2D* backbuffer;
    if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D10Texture2D), (LPVOID*) &backbuffer)))
    {
        //Si erreur
        this->Error("Erreur lors de la récupération du backbuffer.");
        return false;
    }
    if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
    {
        //Si erreur
        this->Error("Erreur lors de la création du render target view.");
        return false;
    }
    backbuffer->Release();
    this->D3DDevice->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiquer où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupéré, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au device le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié aux profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

    //Création du viewport
    ViewPort.Width = width;
    ViewPort.Height = height;
    ViewPort.MinDepth = 0.0f;
    ViewPort.MaxDepth = 1.0f;
    ViewPort.TopLeftX = 0;
    ViewPort.TopLeftY = 0;
    //Assignation du viewport
    this->D3DDevice->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device gràce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le viewport.

  • Gestionnaire Direct3D 11

Le gestionnaire DirectX 11 est constitué d’un fichier DX11Manager.h contenant la définition de la classe et une classe DX11Manager.cpp contenant son implémentation.

  1. Définition de la classe DX11Manager
#pragma once
#include <Windows.h>
#include <D3D11.h>
#include "DXManager.h"

class DX11Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D11Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D11RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D11_VIEWPORT                ViewPort;
    //Context du device
    ID3D11DeviceContext* d3dImmediateContext;

public:
    DX11Manager(void);
    ~DX11Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

Remarque Différences DX10/DX11 : Vous pouvez voir une différence entre la version DirectX 10 et 11. En effet, une nouvelle propriété à été rajoutée (ID3D11DeviceContext). Cela est dû à une nouvelle architecture de DirectX facilitant le multithreading. En effet, jusqu’à DirectX 10, il était impossible d’utiliser plusieurs threads pour envoyer des données à la carte graphique. Avec DirectX 11, il est possible de faire des appels à la carte graphique depuis plusieurs threads à l’aide de contexte.

directx-opengl5-w-157028-13

Chaque thread a un DeviceContext lié au Device. Un thread donne la liste de ses données à afficher à son DeviceContext. Une fois l’ensemble des données fournis, celle-ci sont donné au Device principal qui réalise l’affichage.

2.     Implémentation de la classe DX11Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX
bool DX11Manager::Init(HWND* hWnd)
{
    //Sauvegarde du handle de la fenêtre
    this->hWnd = hWnd;
    
    //Récupération des dimensions de la fenêtre
    RECT rc;
    GetClientRect( *hWnd, &rc );
    UINT width = rc.right - rc.left;
    UINT height = rc.bottom - rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

//Création de la Swap Chain
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));

//Création de deux buffers (un back buffer et un front buffer)
swapChainDesc.BufferCount = 2;
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;;

//Définition du nombre d'image par secondes maximun (60 images).
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

//Multisampling setting
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.SampleDesc.Count = 1;

//Liaison de la swapchain et de le fenêtre
swapChainDesc.OutputWindow = *hWnd;
swapChainDesc.Windowed = true;  

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
D3D_FEATURE_LEVEL FeatureLevels[] = {    
                                        D3D_FEATURE_LEVEL_11_0,
                                        D3D_FEATURE_LEVEL_10_1,
                                        D3D_FEATURE_LEVEL_10_0,
                                        D3D_FEATURE_LEVEL_9_3,
                                        D3D_FEATURE_LEVEL_9_2,
                                        D3D_FEATURE_LEVEL_9_1,
                                    };
int FeatureLevelCount = sizeof(FeatureLevels) / sizeof(D3D_FEATURE_LEVEL);
D3D_FEATURE_LEVEL SelectedFeatureLevel;
HRESULT codeErreur = D3D11CreateDeviceAndSwapChain(NULL, 
                                            D3D_DRIVER_TYPE_HARDWARE, 
                                            NULL, 
                                            D3D11_CREATE_DEVICE_DEBUG, 
                                            FeatureLevels, 
                                            FeatureLevelCount, 
                                            D3D11_SDK_VERSION, 
                                            &swapChainDesc, 
                                            &this->SwapChain, 
                                            &this->D3DDevice,
                                            &SelectedFeatureLevel,
                                            &this->d3dImmediateContext);
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D11CreateDeviceAndSwapChain. Le premier paramètre représente l’adapter à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D11_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D11_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini l’ensemble de FEATURE LEVEL à tester lors de la création du Device. Le paramètre suivant indique le nombre de FEATURE_LEVEL à tester. Les FEATURE_LEVEL sont des niveaux de fonctionnalités disponibles pour une carte. Ainsi, DirectX va tester si elle peut créer un device pour avec les fonctionnalités du premier FEATURE_LEVEL (dans notre cas DX11.0). S’il n’y arrive pas, il va tester avec le FEATURE_LEVEL suivant et ainsi de suite jusqu’à ce qu’il trouve un FEATURE_LEVEL compatible avec la carte.Il renverra l’info dans l’avant dernier paramètre (SelectedFeatureLevel).

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le septième paramètre fourni la structure de description de cette SwapChain.

Les deux suivants paramètres (8/9) sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

Le dernier paramètre permet de récupérer le DeviceContext principal à utiliser. C’est au travers de celui ci que nous communiquerons avec la carte graphique (et non pas avec le D3DDevice comme en DX10).

La méthode D3D11CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

//Création du rendertargetview
ID3D11Texture2D* backbuffer;
if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D11Texture2D), (LPVOID*) &backbuffer)))
{
    //Si erreur
    this->Error("Erreur lors de la récupération du backbuffer.");
    return false;
}
if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
{
    //Si erreur
    this->Error("Erreur lors de la création du render target view.");
    return false;
}
backbuffer->Release();
this->d3dImmediateContext->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiqué où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupérer, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au DeviceContext le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié à la profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

//Création du viewport
ViewPort.Width = width;
ViewPort.Height = height;
ViewPort.MinDepth = 0.0f;
ViewPort.MaxDepth = 1.0f;
ViewPort.TopLeftX = 0;
ViewPort.TopLeftY = 0;
//Assignation du viewport
this->d3dImmediateContext->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device grâce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le Viewport.

  • La Méthode de rendu

L’objectif de ce tutoriel est surtout l’initialisation de Direct3D. Donc le rendu sera ici très sommaire. Nous allons uniquement vider le RenderTarget (donc le BackBuffer) en le remplissant d’une couleur (noire dans notre exemple) puis dire à la SwapChain d’afficher ce BackBuffer.

En DirectX 10, cela se fait comme suit :

//Rendu 3D
void DX10Manager::Render()
{
    this->D3DDevice->ClearRenderTargetView(this->RenderTargetView,D3DXCOLOR(0,0,0,0));
    
    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}

En DirectX 11 :

//Rendu 3D
void DX11Manager::Render()
{
    float clearColor[] = {0,0,0,0};
    this->d3dImmediateContext->ClearRenderTargetView(this->RenderTargetView, clearColor);

    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}
  • Libération des ressources

La création de device, buffer et ainsi … consomme de la mémoire et bloque des ressources. Il est nécessaire, lorsque l’on termine le programme, de libérer ces ressources. Nous allons nous servir du destructeur de la classe pour réaliser ces opérations.

En DX 10 :

//Destructeur
DX10Manager::~DX10Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

En DX 11 :

//Destructeur
DX11Manager::~DX11Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->d3dImmediateContext) this->d3dImmediateContext->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

La méthode Release se charge de libérer les ressources. Le if permet de vérifier si ces ressources n’ont pas déjà été libérées.

  • Conclusion

Nous avons vu comment initialiser Direct3D en Direct 10 et 11. Le résultat à l’exécution, bien que très simpliste, est le suivant :

dxt2-final1

Code source du tutorial

Ce tutorial est encore un peu frustrant (beaucoup de code pour un écran noir). Dans le prochain tutorial, nous allons afficher un triangle à l’écran. Nous verrons par la même occasion un outil indispensable en DirectX 10 / 11 : Les Shaders.

08/11/2010

[DirectX 10/11] Tutoriel 1 : Création d’une application Win32

Filed under: 3D, C++, Débutant, DirectX, Win32 — Étiquettes : , , , , , , — sebastiencourtois @ 00:09

J’ai fait une petite coupure avec ce blog compte tenu des préparatifs de mon départ aux USA et de l’avancement de certains projets persos. Au passage, je voulais souligner la création d’un nouveau blog d’un ex-futur-collègue qui vient de rejoindre la communauté des blogueurs .NET : Gilles Peron.

J’ai aussi décidé de me lancer dans une nouvelle série d’articles sur la programmation en parallèle de celle sur XNA. Le sujet est donc DirectX 10/11. L’une des grosses nouveautés est que nous allons faire ça à l’ancienne : en C++ (oui j’ai dû réapprendre à faire du C++ pour vous Sourire).

  • Introduction à DirectX

DirectX est une boite à outil pour la création de jeu vidéo fournie par Microsoft et fonctionnant principalement sur Windows. DirectX est composé de nombreux catégories chacune étant liée à une partie précise d’un jeu (Direct3D pour le graphisme 3D, DirectInput pour la gestion clavier/souris/manette, DirectPlay pour le réseau …).

Nous en sommes aujourd’hui à la onzième version de DirectX (sorti en 2009). Toutefois, DirectX 11 ajoute des nouvelles fonctionnalités à DirectX 10 qui était une évolution majeure de l’API. C’est pourquoi nous étudierons les deux versions 10 et 11.


  • Installation et configuration du kit de développement DirectX

Pour développer en DirectX, il faut :

    • Un environnement de développement C++
    • Le kit de développement Direct X : Page MSDN DirectX SDK.

Une fois le SDK installé, il suffit de créer  un projet C++ vide et d’ajouter les dossiers includes et libs du DirectX SDK (par défaut dans c:\Program Files\) au projet.

blog_dx_t1_0 blog_dx_t1_1 blog_dx_t1_2
Création d’un nouveau projet C++ vide Ajout du répertoire libs DirectX au projet Ajout du répertoire include DirectX au projet
  • La base d’une application DirectX : La fenêtre

Vous êtes maintenant prêt développer en DirectX. Pour cela, il est nécessaire d’avoir une fenêtre dans laquelle nous allons afficher les informations de notre jeu. Pour cela, plusieurs choix sont possibles. Dans ce tutoriel, je ferais ce que recommande Microsoft dans son tutoriel, l’utilisation de l’API Win32. Cette API complexe existe depuis que Windows existe et gère tout ce qui est fenêtre et gestion des entrées/sortie. D’autres méthodes de gestion de fenêtres plus simple existent (notamment QT) mais elles ne sont pas toutes compatible avec DirectX 10/11.

L’objectif de ce tutoriel va donc être de créer une fenêtre que l’on pourra fermer en appuyant sur la croix de la barre de titre ou en appuyant sur Echap.

Remarque : L’API Win32 et DirectX sont des API distinctes. Win32 est une API qu’il n’est pas spécialement nécessaire de maitriser parfaitement si votre objectif est uniquement de faire de la 3D ou du jeu vidéo. Dans le texte qui suit, je vais essayer de décrire au maximum chacune des propriétés. Il n’est pas nécessaire de mémoriser toute ces informations (ou alors uniquement pour votre culture général).

  • Le point d’entrée de notre programme : WinMain

Tout programme commence toujours par un point d’entrée. En C/C++ classique, ce point d’entrée est écrit ainsi :

#include <stdio.h>
int main(int argc,char **argv)
{
    printf("Hello World !!");
    return 0;
}

La même chose, en Win32, s’écrit de la façon suivante :

#include <stdio.h>
#include <Windows.h>
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    MessageBox(NULL,"Hello World","Titre",MB_ICONERROR);
    return 0;
}

Au niveau des changements entre les deux codes, on voit tout d’abord l’ajout d’un fichier windows.h. Ce fichier contient les fonctions principales de l’API Win32.

La signature du point d’entrée est plus complexe. Cette méthode retourne un nombre au système d’exploitation. La valeur 0 correspond à un programme qui s’est terminé correctement. Le mot APIENTRY signifie que la méthode est une méthode qui peut être appelé (CALLBACK). Le nom de la méthode WinMain est le nom du point d’entrée.Ce nom est défini dans windows.h et est obligatoire pour être reconnu comme un point d’entrée valide. Les deux paramètres suivants correspondent aux instances du programme. La première est l’instance pour le programme que le WinMain va lancer. La deuxième correspond à l’instance du programme qui appel ce WinMain. Dans notre cas, hPrevInstance sera NULL car nous lançons directement l’application (nous ne passons pas par un autre programme pour la lancer.). Le paramètre lpCmdLine correspond aux paramètres qui sont ajouté en arguement au lancement de l’application (équivalent du paramètre argv d’un main C/C++ classique).Pour récupérer la commande complète, vous pouvez aussi utiliser la méthode GetCommandLine. Enfin le dernier paramètre indique comment la fenêtre devra être affiché à son lancement (minimisé, plein écran …). Si vous voulez plus d’informations sur le WinMain, vous trouverez les informations voulues sur la MSDN.

Afin de tester que tout fonctionne correctement, nous allons demander l’affichage d’une boite de dialogue d’erreur. Celle ci est disponible gràce à la méthode MessageBox. Cette méthode prend en premier paramètre le handle (sorte de pointeur) sur la fenêtre qui l’a crée. Dans notre exemple, nous n’avons pas de fenêtre donc nous utiliserons la valeur NULL pour ce paramètre. Ensuite, il est nécessaire de fournir le texte puis le titre que l’on souhaite afficher. Enfin, on peut customiser les icones,boutons de la boite de dialogues avec le dernier paramètre. La liste et l’utilisation de ce paramètre est décrit en détail sur la MSDN.

Lorsque l’on lance ce morceau de code, on obtient le résultat suivant :

blog_dx_t1_3

  • Création d’une fenêtre de base

Nous allons maintenant chercher à créer une fenêtre normale. Pour cela nous allons rajouter 3 variables globales : Un Handle sur la fenêtre, deux nombre représentant la largeur et la hauteur de la fenêtre.

//Handle de la fenêtre
HWND hWnd;        
//Largeur de la fenêtre
int windowWidth = 800;    
//Hauteur de la fenêtre
int windowHeight = 600;

On modifie la méthode WinMain afin de créer et gérer un fenêtre.

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hWnd,hInstance,nCmdShow)) return 0;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
    }
    return (int) msg.wParam;
}

Une première étape va être de créer et d’afficher la fenêtre. Cela va être fait par la méthode InitWindow que nous allons créer dans un moment.

Pour l’instant, nous allons nous concentrer sur la boucle. Windows gère ses fenêtres grâce à un système de message. Ainsi lorsqu’un ordre est donné à une fenêtre (minimiser la fenêtre, la fermer …), le système envoie un message à la fenêtre. Cette boucle regarde si des messages sont disponibles avec la méthode PeekMessage puis analyse le message et agit en conséquence (TranslateMessage) puis dispatch le message à d’autres fenêtres (les fenêtres enfants par exemples). On sort de la boucle lorsque le message de fermeture de l’application (Message WM_QUIT) est reçu par a fenêtre.

La valeur retour du WinMain dépend du wParam du dernier message. En effet, les messages peuvent aussi remonter des codes erreurs. Par conséquent, on utilise cette valeur plutôt que donner la valeur 0 en dur.

Concentrons-nous maintenant sur la création de la fenêtre proprement dite :

//Initialisation de la fenêtre Win32
bool InitWindow(HWND hWnd,HINSTANCE hInstance,int nCmdShow)
{
    //Structure d'information sur une fenêtre
    WNDCLASSEX wndStruct;
    ZeroMemory(&wndStruct,sizeof(WNDCLASSEX));
    wndStruct.cbSize        = sizeof(WNDCLASSEX);
    wndStruct.style            = CS_HREDRAW | CS_VREDRAW;
    wndStruct.lpfnWndProc    = (WNDPROC)wndProc;
    wndStruct.hInstance        = hInstance;
    wndStruct.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wndStruct.hbrBackground    = (HBRUSH)COLOR_WINDOW;
    wndStruct.lpszClassName    = TEXT("TutorialWindow");
    RegisterClassEx(&wndStruct);

    hWnd = CreateWindow( "TutorialWindow", 
                         "Tutorial 1 : Win32", 
                         WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT, 
                         windowWidth, 
                         windowHeight, 
                         NULL, 
                         NULL, 
                         hInstance, 
                         NULL);

    if(!hWnd) return false;

    ShowWindow(hWnd,nCmdShow);
    UpdateWindow(hWnd);

    return true;
}

Afin de créer une fenêtre, il est nécessaire de remplir une structure qui va décrire la fenêtre. Cette structure est WNDCLASSEX. Elle contient un grand nombre de propriété mais nous n’en utiliserons que 7 pour notre exemple.

cbSize Taille de la structure (en octets)
style Indique le comportement de la fenêtre. Dans notre cas, nous demandons à redessiner le contenu de la fenêtre lors qu’il y a  un changement vertical ou horizontal de la fenêtre. Les valeurs possibles sont disponibles ici.
lpfnWndProc Callback permettant de récupérer les évènements de la fenêtre (souris, clavier …). Nous allons créer cette méthode (wndProc) dans le paragraphe suivant.
hInstance Instance de l’application (celle fourni par le WinMain)
hCursor Curseur de souris lorsque celui ci passe sur la fenêtre (IDC_Arrow correspond au curseur de base).
hbrBackground Couleur de fond de la fenêtre
lpszClassName Nom pour la fenêtre. A ne pas confondre avec le titre de la fenêtre. Ce nom de fenêtre sera utilisé lors de la création de fenêtre pour identifier la structure WNDCLASSEX que l’on souhaite utiliser.

Plus d’informations sur la structure WNDCLASSEX sur la MSDN.

Une fois la structure de la fenêtre est définie, il est nécessaire de l’enregistrer dans le système afin de pouvoir la réutiliser. Cela se fait avec la méthode RegisterClassEx.

L’étape suivante est la création du handle de la fenêtre. On utilise pour cela la méthode CreateWindow. Le premier paramètre correspond au nom de la fenêtre comme décrit dans la structure WNDCLASSEX. Le deuxième paramètre est le titre de la fenêtre. Ensuite, on peut choisir le style de la fenêtre (barre de titre, bouton de fermeture …). La liste complète des possibilités se trouve ici. Les quatre paramètres suivants sont la position x,y du coin supérieur gauche de la fenêtre puis sa largeur et hauteur. La constante CW_USEDEFAULT est utilisé ici afin de laisser au système d’exploitation le choix de la position de la fenêtre. Le paramètre suivant indique si cette fenêtre doit être lié à une  autre fenêtre (ce n’est pas notre cas de figure donc NULL). Le NULL suivant correspond au handle d’un menu. Vu que nous ne souhaitons pas de menu, nous mettons une nouvelle fois NULL. Le paramètre qui suit n’est autre que l’instance à laquelle sera lié la fenêtre. Le dernier paramètre n’est pas utile dans notre cas (cas des fenêtres multiples MDI).

La description complète de cette méthode se trouve ici.

Une fois créé, on récupère un handle pour notre fenêtre. Si ce handle est NULL, c’est que la fenêtre n’a pas été créée. Dans le cas contraire, on peut l’afficher avec la méthode ShowWindow auquel on passe le paramètre nCmdShow reçu par le WinMain qui indique comment s’affiche la fenêtre (plein écran, minimisé…).

On appelle enfin la méthode UpdateWindow afin de forcer le rafraichissement de la fenêtre.

  • Réponses aux évènements de la fenêtre

Une fenêtre reçoit des évènements venant des périphériques (clavier,souris). Ce type d’évènements est à prendre en charge dans une méthode indiqué dans la propriété lpfnWndProc de la structure WNDCLASSEX (voir plus haut). Dans notre cas, elle s’appelle wndProc et à toujours la même signature.

LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_KEYDOWN    :    
            if(wParam == VK_ESCAPE)
                PostQuitMessage(0);
        break;        
        case WM_DESTROY    :    
            PostQuitMessage(0);
        break;
    }
    
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Cela fonctionne sur un système de message comme pour la boucle principale sauf que celle ci est spécifique à l’application. Lorsqu’un nouveau message pour la fenêtre arrive, la méthode wndProc est appelée. Nous gérons ici deux messages.

WN_DESTROY est reçu lorsque la fenêtre va être détruite. Cela peut provenir d’un clic sur la croix de fermeture de la fenêtre mais cela peut aussi venir d’autres facteurs comme le fermeture de l’application dans le gestionnaire de tâches ou encore l’extinction de l’ordinateur.

WM_KEYDOWN correspond à une touche du clavier qui a été enfoncée. Le code de la touche est disponible grâce au paramètre wParam.

Vous pouvez trouver ici la liste des messages pour wndProc.

On appelle la méthode PostQuitMessage avec un code retour 0 (aucun problème) pour envoyer un message de fermeture de l’application (donc de la fenêtre). Cela enverra un message WM_Quit à la boucle du WinMain qui sortira du programme.

  • La récompense

Nous sommes arrivés au bout de notre périple. Si vous lancez l’application, vous devriez vous retrouver avec la fenêtre suivante.

blog_dx_t1_4

Si ce n’est pas le cas, je vous donne mon projet de solution (VS 2010).

  • Conclusion

J’imagine votre première réaction à la fin de ce tutoriel : “Tout ça pour ….. ça Triste !!!“. Effectivement le code Win32 est assez indigeste, complexe (j’en connais qui aime ça Sourire) pour un résultat très médiocre. Il s’agit pourtant de la base de tout jeu vidéo DirectX et vous êtes obligé de passer par là pour commencer un projet. La bonne nouvelle est qu’une fois que c’est fait, on n’a plus à s’en préoccuper et on peut réutiliser le même code sans problème. Je vous conseille donc de  garder votre projet sous la main car vous pourriez en avoir besoin très bientôt.

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/

    16/04/2010

    [Nouveautés C# .NET 4] Introduction au C# dynamique

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

    Une des nouveautés qui a fait couler beaucoup d’encre est l’ajout des types dynamiques à .NET 4. Cela venait d’un besoin pour des langages “dynamiques” tel que le Javascript, Python ou encore le Ruby de pouvoir être facilement interopérable avec les langages plus traditionnels comme le C# ou le VB. Microsoft a ainsi rajouter une couche au framework : La DLR (Dynamic Langage Runtime) ainsi qu’un langage supplémentaire tirant entièrement parti de la dlr : le F#.

    • Le principe de fonctionnement

    Le but des types dynamiques de pouvoir appeler des méthodes/propriétés d’un objet et que la résolution (le lien entre le code appelant et le code appelé) ne se fasse pas au moment de la compilation mais que cette résolution se fasse à l’exécution.

    dlr Comme on peut le voir ci dessus, les types .NET sont résolus au moyen d’un “object binder” qui n’est autre qu’un code utilisant la réflexion. Pour les langages dynamiques, chacun a sa propre méthode de résolution.

    Il est à noter quel la DLR s’appuie sur la CLR. Les langages C#/VB peuvent ainsi choisir d’utiliser le couple DLR/CLR ou uniquement la CLR (comme avant). Lorsque la DLR est utilisé, celle ci retraduit les appels pour qu’il soit exécutable par la CLR.

    dlr2

    • Le mot clé dynamic en C#

    Afin de décider le type de runtime nous allons utiliser, C# 4 introduit un nouveau mot clé dynamic. Ce mot clé permet d’indiquer que l’instance de l’objet sera interprété par la DLR et non pas par la CLR directement. Cela permet d’appeler n’importe quel méthode/propriété/indexer de la classe sans connaitre son nom et ses paramètres à la compilation.

    int a = 0;
    dynamic b = a;
    b = 1;
    //a = 0 , b = 1
    List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5 };
    dynamic listDynamic = list;
    listDynamic.Add(6);
    list.Add(7);
    //list = {0,1,2,3,4,5,6,7}
    //listDynamic = {0,1,2,3,4,5,6,7}

    Ce code montre qu’il est possible de convertir tout type (valeur / références) dans des types dynamiques.

    dlr3 Le fonctionnement général de la mémoire n’est pas bouleversé par le mot clé dynamique (les types références vont toujours sur le tas, les types valeurs sur la pile). La seule différence réside au niveau de l’object binder qui,dans le cas de types dynamiques, fera la résolution au moment de l’éxécution du programme.

    public class Test
    {
        public string ValA { get; set; }
        public int ValB;
        public dynamic ValC;
    
        public void Toto(int a){}
        public string Tata(dynamic b) { return string.Empty; }
    }

    Concernant le mot clé dynamic, il est aussi possible de l’utiliser dans les définitions des propriétés,les paramètres de méthodes, les types retours, les indexeurs … bref, on peut l’utiliser dans tous les cas.

    • On pouvait faire du dynamique en C# 3.5 ?

    Il était possible de réaliser ce type d’opération en .NET 3.5. Voici un exemple de code permettant l’appel de méthode dont on passe le nom en paramètres.

    public void CallMethod<T>(T instance, string methodName) where T : class
    {
       Type t = instance.GetType();
       MethodInfo mi = t.GetMethod(methodName);
       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[]{});
    }

    On remarque que le code reste très complexe pour juste appeler une méthode d’une classe. Microsoft nous a donc maché le travail afin de ne plus à avoir utiliser la reflection dans ce genre de cas.

    • Un grand pouvoir implique de grandes responsabilités

    Comme je l’ai dit au début de l’article, cette nouveautés a fait couler beaucoup d’encre. Je suis plutôt contre cette fonctionnalités pour les raisons suivantes :

    1. Lisibilité du code : A l’instar du mot clé var introduit en .NET 3.5, rendre un langage comme C# en partie dynamique va réduire la lisibilité du code car on ne saura plus le type des différentes variables car de nombreux développeurs vont préférer écrire des var/dynamic partout plutôt que typer correctement leurs instances.
    2. Intellisense : Le mot clé dynamic entraine une résolution à l’exécution. Cela veut dire que, au moment de la compilation, on ne dispose d’aucune informations sur la structure des objets que l’on utilise ce qui rend l’écriture du code complexe (une faute de frappe ou de majuscule et on obtient une erreur …. à l’éxécution.)dlr4
    3. Erreur de résolution : Comme expliqué ci-dessus, les fautes de frappes peuvent arriver lorsque l’on code (surtout sans aide de l’intellisense) et on se retrouve avec des erreurs à l’éxécution. Cela introduit un temps supplémentaire car on obtient les erreurs de résolutions après la compilation/exécution. De plus, pour un peu que l’on ne passe pas dans le code où se trouve l’erreur, on ne la voit jamais et c’est l’utilisateur final qui en ait victime.

    dlr5

    • Conclusion

    L’ajout de la DLR est une fonctionnalité sympathique pour l’interopérabilité entre langages statiques et dynamiques. Cela devrait décupler les possibilités des langages .NET et surement entrainer de nouveaux pattern de développement. Toutefois, à l’instal du mot clé var, il se peut que cela soit au détriment de la qualité/lisibilité du code.

    Si vous avez des commentaires/questions sur le sujet, n’hésitez pas à commenter.

    15/04/2010

    [Nouveautés C# .NET 4] Paramètres nommés et optionnels

    Filed under: .NET, Débutant — Étiquettes : , , , , — sebastiencourtois @ 12:43

    Suite de notre tour sur les nouveautés de C# 4. Après une présentation de la Co-Contravariance, nous allons nous intéresser à une fonctionnalité qui faisait cruellement défaut à .NET : les paramètres nommés / optionnels.

    • Paramètres optionnels

    Qui n’a pas écrit ce type de code dans un programme :

    public void MaFonction(int a)
    {
      this.MaFonction(a, "toto");
    }
    public void MaFonction(int a, string b)
    {
       this.MaFonction(a, b, null);
    }
    public void MaFonction(int a, string b, StringBuilder c)
    {
       //Fait le traitement
    }

    une suite (parfois sans fin) de méthodes s’appelant les unes les autres avec des ajouts de paramètres pour arriver à “la fonction qui fait tout”. Cela a pour désavantage d’augmenter sensiblement (et inutilement) la taille du code source et cela peut être la source d’erreurs dans l’attribution des paramètres.

    Pour parer à ces inconvénients, Microsoft a introduit, dans .NET 4, une nouvelle fonctionnalité : les paramètres optionnels. Il s’agit de définir des valeurs par défaut aux paramètres que l’on souhaite définir comme optionnel.

    Le code précédent peut donc s’écrire en .NET 4 :

    public void MaFonctionNET4(int a, string b = "Toto", StringBuilder c = null)
    {
        //Fait le traitement
    }

    Au niveau de l’Intellisense, on se retrouve avec les affichages suivants : 

    params1params2

    Intellisense pour la version classique

    Intellisense pour la version .NET 4

    Remarque : Les paramètres optionnels doivent toujours apparaitre après les paramètres obligatoires. Ainsi le code suivant génère une erreur de compilation.

    params3 
    • Paramètres nommés

    Lorsque l’on a des méthodes avec beaucoup de paramètres, il arrive que l’on ne se rappelle plus de l’ordre de chacun des paramètres. Récupérer l’information par l’IntelliSense ou en allant voir dans le code source peut prendre parfois un peu de votre temps au combien précieux.

    Pour pallier ce problème, .NET 4 permet d’utiliser les paramètres nommés.

    Prenons une méthode classique :

    public void MaFonctionNET4WithName(string param1, int param2 = 0, double param3 = -42.424242)
    {
    
    }

    .NET 4 permet de définir les paramètres en fonction de leur nom et non plus en fonction de leur ordre. On peut ainsi écrire :

    t.MaFonctionNET4WithName(param1: "Test",param2: 5);

    Il est à noter que les paramètres nommés et les paramètres optionnels peuvent cohabiter.

    Remarque : Lorsque l’on utilise les paramètres nommés pour l’appel d’une méthode, il faut le faire sur l’ensemble des paramètres il faut indiquer les paramètres nommés après les paramètres classiques (qui, eux, doivent être dans l’ordre)  . Dans le cas contraire,on récupère l’exception suivante. Dans le cas contraire,on récupère l’exception suivantes.

    params4 

    En revanche, l’exemple suivant est valide :

      t.MaFonctionNET4WithName("Toto", param3: 34.45, param2: 8); // Fonctionne car les paramètres non nommés sont en premiers
    

    • Conclusion

    J’attendais cette fonctionnalité depuis un moment (surtout parce que je l’avais déjà utilisé dans d’autres langages). Il faut avouer que c’est une fonctionnalité simple,rapide à mettre en place et qui apporte une meilleur lisibilité et stabilité au code produit.

    Merci à François Guillot pour avoir détecté l’erreur concernant l’ordre des paramètres.

    14/04/2010

    [Nouveautés C# .NET 4] Introduction à la Covariance / Contravariance

    Filed under: .NET, Débutant, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 14:44

    Pour la sortie de .NET 4, je vais faire quelques articles sur les nouveautés du langages (en attendant de pouvoir jouer avec SL4). Aujourd’hui, nous allons voir la notion de Covariance / Contravariance.

    EDIT (08/05/2010) : La Covariance/Contravariance n’est pas disponible pour Silverlight 4

    • Définitions

    La covariance et la contravariance dixit Wikipedia :

    Within the type system of a programming language, a typing rule or a type conversion operator is:

    • covariant if it preserves the ordering, ≤, of types, which orders types from more specific to more generic;
    • contravariant if it reverses this ordering, which orders types from more generic to more specific;
    • invariant if neither of these apply.

    Bon, je suis d’accord qu’avec cette définition, on est pas plus avancé. On voit que cela parle de conversion de type et de généricité mais ça reste très abstrait.

    Nous allons donc voir des exemples puis revenir sur ces définitions.

    • Exemples sur la covariance 

    Nous prendrons une architecture simple pour ces exemples :

    public abstract class Person { }
    public class Employe : Person { }
    public class Customer : Person { }
    public class Manager : Employe { }

    Nous allons maintenant jouer avec des tableaux de ces objets :

    Person[] MaSociete = new Manager[10];  // .NET 3.5 / 4 : OK 
    Employe[] MaSociete2 = new Manager[10]; // .NET 3.5 / 4 : OK 
    

    Rien de transcendant là dessus. On a un tableau de Manager. Un manager étant une personne (ça dépend dans quel société …:)), il n’y a pas de problème à mettre les données d’un tableau de manager dans un tableau de personnes. De même pour mettre nos données manager dans le tableau d’employés (Manager étant un employé).

    Faison la même chose avec des listes

    Manager[] MaSociete3 = new Manager[10];
    IEnumerable<Manager> im = MaSociete3.Where(p => p.ToString() == "Manager"); // .NET 3.5 / 4 : OK 
    IEnumerable<Manager> im2 = new List<Manager>(); // .NET 3.5 / 4 : OK 

    Le requête LINQ renvoie un IEnumerable<Manager> que je peux donc mettre dans im. De même pour une liste de manager car List<T> implémente IEnumerable<T>.

    IEnumerable<Person> ip = im;  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    

    Sur cette ligne, on obtient une erreur de compilation en .NET 3.5 (Cannot Convert IEnumerable<Manager> to IEnumerable<Person>). Pourtant cela devrait marcher comme un tableau (un manager est une personne donc un tableau de personne peut contenir des données venant d’un tableau de manager). Cela n’est pas possible car, en .NET 3.5, IEnumerable<T> n’est pas covariant (à l’inverse de Array qui lui est covariant). C’est pour corriger ce manque que Microsoft a autorisé la covariance des interfaces et des délégués en .NET 4.

    En .NET 4, je peux ainsi écrire :

    IEnumerable<Person> ipl = new List<Manager>();  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    Func<string> funcString = () => { return "Bonjour le monde"; };
    Func<object> funcObject = funcString;         // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    

    Pour résumer, on peut dire que la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie (nous reviendrons sur cette notion plus loin).

    Remarque :  La covariance (ainsi que la contravariance) fonctionne uniquement avec des interfaces et des délégués. Les arguments génériques doivent être des types références. Il n’est pas possible de faire les lignes suivantes :

    List<Person> lo = new List<Manager>();         // .NET 3.5 / 4 : Erreur de compilation 
    IEnumerable<ValueType> lo = new List<int>();    // .NET 3.5 / 4 : Erreur de compilation 
    

    • Exemples sur la contravariance 

    Dans certains cas, on aimerait que l’inverse soit possible.

      Action<object> actObject = (object o) => { };
      Action<string> actString = actObject;          // .NET 3.5 : Erreur de compilation  / .NET 4 : OK

    Dans cet exemple, je souhaite pouvoir caster mon délégués avec une valeur de retour de type string ou un de ses parents. En effet,lorsque je récupère une donnée, je peux la caster dans une variable d’un type dont elle hérite. C’est le principe de la contravariance introduite dans .NET 4.

    La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée (nous reviendrons sur cette notion plus loin).

    • Les interfaces / Délégués déja covariant et contravariant

    Voici la liste des interfaces/délégués ayant été mis à jour au sein du framework .net 4 pour tenir compte de cette nouvelle possibilité.

     

    • Exemple pratique : La création d’une interface covariant et contravariant

    Prenons l’exemple d’une classe réalisant de la transformations de données.

    public interface IDataTransformer<T, K>
    {
        K Transform(T data);
    }
    
    public class DataTransformer<T, K> : IDataTransformer<T, K>
    {
        public K Transform(T data)
        {
            return default(K);
        }
    }

    Nous avons donc une interface IDataTransformer comprenant deux paramètres génériques T et K et une méthode qui va transformer une variable de type T en type K. Nous allons créer un type implémentant cette interface puis l’utiliser.

    IDataTransformer<Manager, string> dataTransformer = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : OK
    IDataTransformer<Person, string> dataTransformer2 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 
    IDataTransformer<Manager, object> dataTransformer3 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 
    

    Si le premier cas fonctionne logiquement, les deux cas suivants sont  moins logique. Dans le cas de dataTransfomer2, si on crée un DataTransformer prenant un Manager en entrée, on devrait aussi pouvoir prendre une personne (rappelons le, les managers sont des personnes).Pourtant le compilateur n’est pas de cette avis car il n’arrive pas à convertir DataTransformer<Manager,string> en IDataTransformer<Person,string>. De même pour dataTransformer3. Si on récupère en sortie une chaine de caractère, on pourrait très bien récupérer un object car string dérive de object.

    Voila les limites de .NET 3.5 et voyons comment faire pour corriger cela en  .NET 4. Pour cela nous allons créer une nouvelle interface et une nouvelle classe Variant.

    public interface IDataTransformerVariant<in T,out K>
    {
        K Transform(T data);
    }
    
      
    public class DataTransformerVariant<T, K> : IDataTransformerVariant<T, K>
    {
        public K Transform(T data)
        {
            return default(K);
        }
    }

    La différence se situe uniquement dans la déclaration de l’interface IDataTransformerVariant. On a rajouté un mot clé in devant le type d’entrée (T) et un out devant le type de sortie (K). En procédant ainsi, on active la covariance et la contravariance et on oblige le paramètre générique T a toujours être en paramètres de méthodes et le paramètre générique K a toujours être en valeur retour d’une méthode. Ne pas suivre cette règle entraine l’erreur de compilation suivante :

    CoVarImg1

    • Conclusion

    Pour conclure, je reprendrais les phrases importantes de cet article :

    la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie.

    La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée.

    Le fait que un paramètre générique soit d’entrée ou de sortie dépend de son placement dans les méthodes interfaces ou des délégués.

    A titre d’information, ce concept est disponible depuis .NET 2.0 en IL (comme l’à montré Simon Ferquel dans son blog) mais aucun mot clé n’étant fourni en .NET, on ne pouvait pas l’utliser en C#.

    Merci à Alexis Pera (MSP) pour la relecture et correction de cet article.

    25/02/2010

    Erreur Visual Studio : “The following module was built either with optimizations enabled or without debug information”

    Filed under: .NET, Débutant, Hors Catégorie, Visual Studio — Étiquettes : , , , , — sebastiencourtois @ 13:31

    Un collègue est tombé sur ce problème ce matin en voulant débugger un service.

    Debug

    Remarque : Cette boite de dialogue est généralement accompagné d’un nom de dll/exe. Elle a été enlevé ici de la copie d’écran.

    Apparemment Visual Studio n’arrive pas à rentrer en mode DEBUG. Dans ce cas là, plusieurs choses à vérifier tout de suite :

    1. Vérifier l’onglet Build du fichier csproj du projet concerné (indiqué par le nom de la dll).
      • Vérifier que votre configuration est bien sur “Debug”
      • Vérifier que la case “Optimize code” est décoché (même en Debug, on ne peut pas débugger du code optimisé).
      • Vérifier que le résultat de la compilation va bien dans le répertoire que vous souhaitez (Output Path)
      • Cliquer sur le bouton “Advanced” et vérifier que “Debug Info” est sur “Full”.

    CsprojBuild

    2. Dans le répertoire de sortie du projet, vérifier que la DLL est bien accompagné d’un fichier pdb du même nom. Ce fichier contient les symboles permettant le debug de l’assembly. Si ce n’est pas le cas, faites un “clean project” puis “Build Project”. Ces fichiers devraient apparaitre.

    2bis. Si ce n’est toujours pas le cas,voir point 4.

    3. Vous avez un fichier pdb dans votre répertoire de sortie mais pas dans le répertoire où est utilisé votre assembly. Un test rapide est de copier le pdb dans le répertoire où est utilisé l’assembly et lancer l’application (F5). Si cela fonctionne, passer au point 4 pour automatiser la copie.

    4. Depuis Visual Studio, supprimer la référence de la dll incriminée des projets puis rajouter la de nouveau. Un petit Clean/Rebuild Solution devrait tout faire rentrer dans l’ordre.

    Je ne sais pas exactement la raison de ce problème.Apparemment “sans raison”, Visual Studio conserve une ancienne version de la dll et/ou ne transfère pas les fichiers pdb. Dans le cas de mon collègue, le point 4 a permis la résolution du problème.

    En espérant que ce post aidera des gens. Si quelqu’un à une explication sur la raison du non transfert des fichiers pdb, je suis intéressé.

    05/05/2009

    Introduction à Small Basic en français.

    Filed under: .NET, Débutant, Small Basic — Étiquettes : , , , , , , — sebastiencourtois @ 09:21

    Après plusieurs semaines de travail, une équipe de MSP a traduit la documentation anglaise de Small Basic en français.

    Pour ceux qui ne connaissent pas Small Basic, il s’agit un projet de Microsoft fournissant aux débutants en programmation un environnement simple pour commencer doucement dans ce monde parfois obscur. Avec une syntaxe proche du Visual Basic, ce langage contient de nombreuses classes simplifiées fournissant facilement les fonctionnalités réellement nécessaires aux débutants.

    SB

    Petit tour de l’équipe qui a réalisé la traduction :

    Au passage, un grand merci à Florie Perouze de l’équipe Education de Microsoft France qui nous a permis de réaliser ce projet.

    Site officiel Small Basic

    Blog officiel Small Basic

    Documentation Français

    24/04/2009

    [Virtual Earth] Développement Microsoft Virtual Earth Web Services – Part 4 : Route Service

    Filed under: .NET, Débutant, Virtual Earth — Étiquettes : , , , , , — sebastiencourtois @ 15:01

    Liens des posts précédents :

    1. Introduction à Microsoft Virtual Earth (Part 1)
    2. Géocoding / Géocoding Inverse (Part 2)
    3. Récupération des images satellites (Part 3)
    4. Calcul d’itinéraires (Part 4)

    Suite de notre grande exploration du Web Service Virtual Earth. Aujourd’hui, nous allons nous intéresser au calcul d’itinéraire. Cette fonctionnalité de Virtual Earth permet de calculer le trajet entre une série de point de contrôles (Waypoints). Il est possible de régler les options du calcul d’itinéraire (type de déplacement, trajet le plus rapide, le plus court …).

    Comme d’habitude, il faut ajouter faire “Add Service Reference” avec l’adresse : http://staging.dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc 

    Remarque : Afin de fonctionner, il est nécessaire régler certains paramètres WCF de ce service. Dans le fichier app.config (ou web.config ou encore ServiceReference.clientconfig selon le type de projet) de l’application, dans la partie contenant les Binding WCF, il faut trouver le binding lié à RouteService (en général : <binding name="BasicHttpBinding_IRouteService") puis modifier l’attribut maxReceivedMessageSize (mettre la valeur 9000000 à la place de celle par défaut) et l’attribut maxBufferSize (mettre la valeur 9000000 à la place de celle par défaut). Cela est du au fait que RouteService renvoie un nombre d’informations supérieur à ce dont WCF est habitué à manipuler.

    Ce service contient une classe principale RouteServiceClient  qui contient deux méthodes :

        • CalculateRoute(RouteRequest request) : Calcul d’un itinéraire entre plusieurs points
        • CalculateRoutesFromMajorRoads(MajorRoutesRequest request) : Calcul d’itinéraires partant des routes principales vers un point d’arrivée spécifié (carte d’accès).

     


    • Calcul d’un itinéraire entre plusieurs points

    Le calcul d’itinéraire entre plusieurs points à besoin de deux informations : Les points de passages et là façons de calculer le trajet. Ces deux informations sont contenues dans la classe RouteRequest qui sera passer en paramètre de la méthode CalculateRoute de RouteServiceClient.

    Les points de passages doivent être fournis dans la propriété WayPoints de  RouteRequest. Cette propriété prend un tableau de WayPoint, un WayPoint n’étant composé que d’une localisation GPS (latitude/longitude/altitude) et d’une propriété Description pour donner un titre au point de passage.

    Les options de calcul d’itinéraire se trouvent dans la propriété Options. Cette propriété est de type RouteOptions et contient les champs suivantes :

    Nom de la propriété Description
    Mode Type de déplacement.
    Enumération TravelMode :
    Driving : Voiture (par défaut)
    Walking : A pied (ne marche pas pour tous les pays)
    Optimization Méthode de calcul du trajet. Marche en combinaison avec TrafficUsage
    Enumération RouteOptimization :

    MinimizeDistance : Distance minimale
    MinimizeTime : Temps minimal

    RoutePathType Type de données brutes renvoyées (en plus des données d’itinéraires)
    Enumération RoutePathType :
    None : Aucune donnée (par défaut)
    Points : Un ensemble de points décrivant le chemin de la route.
    TrafficUsage Type d’adaptation du trajet en fonction du trafic
    Enumération TrafficUsage :

    None : On ne tient pas compte du trafic (par défaut)

    TrafficBasedRouteAndTime : On change le trajet pour minimiser le temps si nécessaire

    TrafficBasedTime : On ne change pas la route mais on tient compte du trafic lors du calcul du trajet.

    Combinaisons de Optimization et TrafficUsage :

    Optimization TrafficUsage Description

    MinimizeDistance

    None

    Trajet le plus direct (en distance) sans prendre en compte le trafic.

    MinimizeDistance

    TrafficBasedRouteAndTime

    Une Exception lancée. Je pense que c’est du au fait que changer la route d’un trajet le plus direct en fonction du trafic => trajet plus long => incohérence.

    MinimizeDistance

    TrafficBasedTime

    Trajet le plus direct (en distance) en tenant compte du trafic dans les calculs de durée (le trajet n’est pas changé… on passera dans les bouchons quand même :)).

    MinimizeTime

    None

    Trajet le plus rapide (en temps) sans prendre en compte le trafic.

    MinimizeTime

    TrafficBasedRouteAndTime

    Trajet le plus rapide (en temps) en tenant compte du trafic et en le modifiant le cas échéant.

    MinimizeTime

    TrafficBasedTime

    Trajet le plus rapide (en temps) en tenant compte du trafic dans les calculs de temps de trajet (la route n’est pas modifié en fonction du trafic).

    Un petit code vaut mieux qu’un long discours : (les variables positionWinwise, positionMSFUniv, positionMSF sont des coordonnées géographiques calculées avant ce code)

    RouteServiceClient rsc = new RouteServiceClient();
    RouteRequest rq = new RouteRequest();
    rq.Credentials = new VETutorial.RouteService.Credentials();
    rq.Credentials.Token = token;
    rq.Culture = "fr-FR";
    rq.Waypoints = new Waypoint[]
    {
    	new Waypoint() 
    	{ 					
    		Description = "Winwise (Départ)", 
    		Location = new VETutorial.RouteService.Location() 
    		{
    			Longitude = positionWinwise.Longitude,
    			Latitude = positionWinwise.Latitude,
    		}
    	}, 
    	new Waypoint() 
    	{ 
    		Description = "Microsoft France (Rue de l'université)", 
    		Location =  new VETutorial.RouteService.Location() 
    		{
    			Longitude = positionMSFUniv.Longitude,
    			Latitude = positionMSFUniv.Latitude,
    		}
    	}, 
    	new Waypoint() 
    	{ 
    		Description = "Microsoft France (Rue du québec)", 
    		Location =  new VETutorial.RouteService.Location() 
    		{
    			Longitude = positionMSF.Longitude,
    			Latitude = positionMSF.Latitude,
    		}
    	},
    };
    rq.Options = new RouteOptions()
    {
    	Mode = TravelMode.Driving,
    	Optimization = RouteOptimization.MinimizeTime,
    	RoutePathType = RoutePathType.Points,
    	TrafficUsage = TrafficUsage.TrafficBasedRouteAndTime,
    };
    RouteResponse routeResult = rsc.CalculateRoute(rq);

    Comme vous pouvez voir dans ce code, on peut définir des informations supplémentaires comme les données d’authentifications ou encore la culture (langue) du résultat.

    En réponse à l’appel de la méthode CalculateRoute, on obtient une classe de réponse de type RouteResponse contenant un champ ResponseSummary (déjà décrit dans les posts précédents) et un champ Result de type RouteResult contenant les informations sur le trajet.

    La classe RouteResult fonctionne en arborescence :

    Un RouteResult contient des Legs: Un Leg  est un trajet entre deux points de passages. Si plusieurs points de passages alors plusieurs Legs dans le RouteResult

    Un Leg contient un tableau d’ItineraryItem (dans la propriété Itinerary) : Un ItineraryItem contient les informations entre deux changements de directions.

    Chaque RouteResult, Leg et ItineraryItem contient une propriété Summary contenant le rectangle de leur localisation (propriété BoundingRectangle) ainsi que le temps de parcours en secondes (propriété TimeInSeconds) et la longueur de ce parcours dans la propriété Distance (par défaut en KM mais réglable dans la propriété UserProfile de la requête).

    En dehors de ces données, la classe RouteResult contient aussi les données brutes du chemin dans la propriété RoutePath. Il contient un tableau de points géographiques représentant le trajet à faire. Cette propriété est la raison pour laquelle les données renvoyés par le service sont énormes. N’activer cette propriété que si nécessaire (désactivé par défaut : propriété RoutePathType de la requête).

    La classe Leg a aussi deux propriétés spécifiques : ActualStart, ActualEnd. Ces propriétés indiquent les positions géographiques de départ et d’arrivés du Leg.

    La classe ItineraryItem contient les propriétés suivantes :

    Nom de la propriété Description
    CompassDirection Orientation du déplacement (chaines de caractères “Nord” …)
    Hints Indication permettant de mieux repérer le changement de direction (exemple : “la rue précédente est …”)
    Location Position du changement de direction
    ManeuverType Type de changement (tourner à gauche, droite, demi tour …)
    Text Texte en format XML indiquant le changement à faire (Exemple : “<VirtualEarth:Action>Quitter</VirtualEarth:Action> <VirtualEarth:RoadName>Rue Gaillon</VirtualEarth:RoadName>”)
    Warnings Danger sur la route. Lié aux infos traffic, cette propriété indique s’il y a un évènement sur votre trajet (accident, travaux …)

    Voici le code d’affichage d’un trajet :

    Console.WriteLine("--- Itinéraire  ---");
    Console.WriteLine("Temps de calcul : "+ sw.Elapsed);
    int i = 1;				
    foreach (var leg in routeResult.Result.Legs)
    {							
    	var duree = new TimeSpan(0,0,(int)leg.Summary.TimeInSeconds);
    	Console.WriteLine("-----    Partie " + i + " ------");
    	Console.WriteLine("De (" + leg.ActualStart.Latitude +","+leg.ActualStart.Longitude+") à (" + leg.ActualEnd.Latitude +","+leg.ActualEnd.Longitude+")");
    	Console.WriteLine("Distance " + leg.Summary.Distance + " km");
    	Console.WriteLine("Temps estimé : " + duree.Hours + " h " + duree.Minutes + " min " + duree.Seconds+ " sec");
    	Console.WriteLine("************ TRAJET ************");
    	int j = 1;
    	double distanceDone = 0;
    	foreach (var turn in leg.Itinerary)
    	{			
    		var duree2 = new TimeSpan(0, 0, (int)turn.Summary.TimeInSeconds);
    		string HumanInfos = StripXMLFromText(turn.Text);
    		Console.WriteLine("Indication " + j + " " + (distanceDone) + " km ["+duree2.Hours+":" + duree2.Minutes + ":" + duree2.Seconds+ "] : " + HumanInfos);
    		distanceDone += turn.Summary.Distance;
    		j++;
    	}
    	Console.WriteLine("----- Fin Partie " + i + " -----");
    	i++;
    }

    Le résultat obtenu est le suivant :

    — Itinéraire  —

    Temps de calcul : 00:00:04.3373465

    —–    Partie 1 ——

    De (48,868681,2,334231) à (48,861122,2,309237)

    Distance 2,788 km

    Temps estimé : 0 h 7 min 46 sec

    ************ TRAJET ************

    Indication 1 0 km [0:0:11] : Quitter Rue Gaillon

    Indication 2 0,13 km [0:0:49] : Prendre à gauche Avenue de l’Opéra

    Indication 3 0,156 km [0:0:53] : Tourner à droite Rue Saint-Roch

    Indication 4 0,569 km [0:1:17] : Tourner à droite Rue de Rivoli

    Indication 5 1,215 km [0:0:5] : Continuer tout droit Place de la Concorde

    Indication 6 1,257 km [0:0:9] : Continuer à gauche Place de la Concorde

    Indication 7 1,338 km [0:0:58] : Au rond-point prendre 4

    Indication 8 1,61 km [0:0:57] : Prendre à droite Pont de la Concorde

    Indication 9 1,835 km [0:1:17] : Tourner à droite Quai d’Orsay

    Indication 10 2,531 km [0:0:6] : Continuer à droite Quai d’Orsay

    Indication 11 2,584 km [0:0:2] : Tourner à gauche Quai d’Orsay

    Indication 12 2,61 km [0:0:56] : Continuer tout droit Rue Surcouf

    Indication 13 2,777 km [0:0:6] : Tourner à droite Rue de l’Université

    Indication 14 2,788 km [0:0:0] : Arrivée Microsoft France (Rue de l’université)

    —– Fin Partie 1 —–

    —–    Partie 2 ——

    De (48,861122,2,309237) à (48,6900826,2,2157051)

    Distance 33,094 km

    Temps estimé : 0 h 33 min 46 sec

    ************ TRAJET ************

    Indication 1 0 km [0:0:36] : Quitter Rue de l’Université

    Indication 2 0,28 km [0:0:28] : Tourner à droite Rue Malar

    Indication 3 0,439 km [0:1:9] : Tourner à gauche Quai d’Orsay

    Indication 4 0,685 km [0:2:26] : Continuer tout droit Quai Branly

    Indication 5 2,079 km [0:1:27] : Le nom de la voie change Quai de Grenelle

    Indication 6 2,877 km [0:2:34] : Le nom de la voie change Quai André Citroën

    Indication 7 4,109 km [0:0:33] : Au rond-point prendre 1

    Indication 8 4,378 km [0:0:55] : Le nom de la voie change Quai d’Issy-les-Moulin

    eaux

    Indication 9 4,925 km [0:5:21] : Voie Ad/15 prendre à gauche Boulevard Périphéri

    que Lyon

    Indication 10 10,008 km [0:2:57] : Prendre à droite E15 Nantes-Bordeaux/Aéroport

    Orly-Rungis/Évry-Lyon

    Indication 11 12,67 km [0:2:14] : Prendre à droite E15 Véhicules Lents-Toutes Di

    rections

    Indication 12 15,678 km [0:2:11] : Continuer à gauche E15

    Indication 13 19,625 km [0:7:10] : Prendre à droite E5/E50

    Indication 14 30,882 km [0:0:54] : 9 prendre à droite Chartres Par RN/Les Ulis/Z

    .A. de Courtaboeuf/Villejust

    Indication 15 31,465 km [0:0:31] : Prendre à droite D118

    Indication 16 31,913 km [0:0:15] : Prendre à droite

    Indication 17 32,063 km [0:0:36] : Tourner à droite Avenue de la Baltique

    Indication 18 32,32 km [0:1:29] : Tourner à droite Avenue du Québec

    Indication 19 33,094 km [0:0:0] : Arrivée Microsoft France (Rue du québec)

    —– Fin Partie 2 —–

    J’avoue que c’est moins sexy qu’une belle carte en 3D avec le chemin en surbrillance mais c’est mieux que rien. Je vais essayer de voir comment afficher ce trajet sur une carte (avec le RoutePath de RouteResult je pense). Sujet d’un prochain post.


    • Calcul d’itinéraires depuis des routes principales

    Le principe de la méthode CalculateRoutesFromMajorRoads est de fournir les trajet allant (ou revenant) d’un point nommé Destination vers les grands axes routiers. Au niveau de la requête, la plupart des propriétés décrites ci dessus fonctionnent avec la classe MajorRoutesRequest. Deux différences toutefois :

    La propriété Waypoints disparait au profit d’une propriété Destination ne permettant de définir qu’un WayPoint.

    La propriété Options devient du type MajorRoutesOptions et cela ajoute un nouveau paramètre d’options : la propriété booléenne ReturnRoutes indique si on souhaite calculer les routes entre la destination et les routes majeurs (true) où si l’on souhaite uniquement les points d’entrées sur les routes majeures les plus proches (false. valeur par défaut).

    MajorRoutesRequest mrr = new MajorRoutesRequest();
    mrr.Credentials = new VETutorial.RouteService.Credentials();
    mrr.Credentials.Token = token;
    mrr.Destination = new Waypoint()
    {
    	Description = "Winwise (Départ)",
    	Location = new VETutorial.RouteService.Location()
    	{
    		Longitude = positionWinwise.Longitude,
    		Latitude = positionWinwise.Latitude,
    	}
    };
    mrr.Culture = "fr-FR";
    mrr.Options = new MajorRoutesOptions()
    {			  
    	Mode = TravelMode.Driving,
    	Optimization = RouteOptimization.MinimizeTime,
    	RoutePathType = RoutePathType.Points,
    	TrafficUsage = TrafficUsage.TrafficBasedRouteAndTime,
    	ReturnRoutes = true
    };
    MajorRoutesResponse MRresult = rsc.CalculateRoutesFromMajorRoads(mrr);

    Au niveau de la réponse, on reste sur les mêmes types de données. On a ainsi la propriété Routes contenant des RouteResult (décrit plus haut) donnant les itinéraires entre le point destination et une route majeure. La propriété StartingPoints est un tableau de positions géographiques indiquant les entrées sur les routes majeures environnantes.

    Ici se conclut ce post sur les calculs d’itinéraire sous Virtual Earth. Il ne reste plus qu’un service à découvrir : Le SearchService puis nous terminerons par une belle application WPF reprenant tout ce que l’on a appris aux cours de ces 5 posts.

    N’hésitez pas à mettre des commentaires pour enrichir ces tutoriaux.

    Older Posts »

    Propulsé par WordPress.com.

    %d blogueurs aiment cette page :