Preparando o Dispositivo

por :Murilo Maciel Curti

 

Este artigo tem como objetivo mostrar como dar o primeiro passo no desenvolvimento de Jogos e interfaces gráficas avançadas utilizando a linguagem C++ com a API gráfica DirectX.

Veremos aqui como preparar o nosso "Dispositivo"(Device) para receber a renderização de nossas cenas.

 

Utilizarei aqui o Visual C++ 2005, que atualmente se encontra dem versão Beta e pode sofrer algumas alterações até seu lançamento, mas acredito que nada que possa atrapalhar a utilização deste artigo.

 

Antes de mais nada é necessários prepararmos o nosso projeto Win32 para trabalhar com o DirectX nesta versão, pois esta versão vem com uma parâmetro que pode gerar muita dor de cabeça.
Nas propriedades do projeto faça:

Configuration Properties/General/Character Set -> Not Set

 

Podmos já deixar o projeto preparado para as possíveis dependências, em

Configuration Properties/Linker/Input -> d3dxof.lib dxguid.lib d3dx9d.lib d3d9.lib winmm.lib

 

Iniciamos com a inclusão das bibliotecas necessaries, para trabalharmos simplesmente com o Dispositivo, utilizamos o cabeçalho padrão do Direct 3D.

 

#include

#include

 

Estamos iniciando uma classe que precisa ter uma identificação perante o sistema operacional, podemos definir uma macro que carregue esta identificação para ser utilizada na inicialização e encerramento da janela.

 

#define SHARP_CLASS_NAME "SharpDeviceWindow"

 

A utilização desta macro facilita a leitura e manutenção do código, veremos mais à frente que ela é utilizada em alguns pontos essenciais do programa.

Agora preciamos começar a pensar no nosso Device, e para ele ser utilizado por toda o programa declaramos como globais um objeto do Direct3D que cuidará da renderização das cenas no nosso Device.

 

LPDIRECT3D9 g_pD3D = NULL; // Used to create the D3DDevice

LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; // Our rendering device

 

Objetos prontos e “inicializados” precisamos crier uma função que se responsabilizará pela inicialização do Direct3D. O seu valor de retorno é HRESULT, onde devolveremos S_OK se conseguirmos inicializar o Direct3D com sucesso ou E_FAIL se ocorrer algum erro, assim podemos fazer um tratamento eficaz sobre o estado do nosso dispositivo.
Começamos iniciando o objeto responsável pela criação do nosso dispositivo, é necessário termos uma instância válida para que possamos passer os dados corretos sobre a compatibilidade de hardware para o dispositivo.

 

HRESULT InitD3D( HWND hWnd )

{

    // Cria o objeto do Direct3D

    if((g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)) == NULL)

        return E_FAIL;

 

Obtendo êxito na criação do objeto, podemos declarar a estrutura D3DPRESENT_PARAMETERS, que é responsável pelas propriedades da tela. Com ela podemos definer a quantidade de cores, as configurações dos Buffers, formato de apresentação entre outros.

D3DPRESENT_PARAMETERS d3dpp;

 

Como alguns parâmetros não serão definidos, existe a necessidade de “zerá-los” para que não passem dados inválidos para o nosso dispositivo, a função ZeroMemory cuida disto, ela faz com que todos os bytes encontrados na região reservada para a estrutura sejam setados para 0, assim podemos trabalhar tranquilamente sem a preocupação com alguma anomalia inesperada.

 

ZeroMemory(&d3dpp, sizeof(d3dpp));

 

Passamos um ponteiro que aponta para a região da memória onde é iniciado o espaço de nossa estrutura e utilizamos a função sizeof para informar a quantidade de bytes que correspondem à declaração de nossta estrutura. Atentem para estas funções, elas serão amplamente utilizadas no desenvolvimento de jogos.

 

O primeiro parâmetro que nos interessa é o responsável pelo modo de apresentação da janela, neste exemplo queremos que ela seja apresentada da forma convencional, uma janela, com os botões padrões.

 

d3dpp.Windowed = TRUE;

 

Para este tipo de exibição devemos usar o SwapEffect em sua forma padrão, onde ele informa ao Direct3D que ele deve escolher o melhor metodo para fazer a troca entre back e front buffer. Caso queiramos uma janela em modo FullScreen, devemos tomar alguns cuidados, que serão abordados no próximo artigo.

 

d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

 

Também Podemos, e devemos definir qual é o formato de cores do nosso Back Buffer. Assim podemos definer se serão utilizadas 16 ou 32 bits de cores. É preciso tomar cuidado com este parâmetro pois nem todos os formatos são compatíveis com as placas de videos, então deixar o Direct3D fazer este trabalho por nós é interessante, pelo menos para este exemplo.

 

 

D3dpp.BackBufferFormat = D3DFMT_UNKNOWN;

 

Assim o Direct3D utilize o formato exibido na tela atualmente, porém este método só é válido para a janela vista como Windowed.
Para outras opções de cores utilize D3DFMT_R5G6B5 para 16 bits e D3DFMT_X8R8G8 para 32 bits.

 

Para finalizar a função agora vamos crier o nosso dispositivo, que vai ser criado baseando se nos dados da placa padrão D3DADAPTER_DEFAULT, geralmente encontramos apenas uma placa em cada máquina, mas se houver mais de uma não haverá problema algum.

Também instruimos o Direct3D a usar o máximo de processamento de Hardware possível, baseando se nas capacidades do sistema em qual está atuando, tendo assim processamento de software onde houver incompatibilidade e utilizar o máximo de hardware possível, fazendo a mixagem necessária para a rasterização, D3DDEVTYPE_HAL.

Também utilizamos aqui o processamento de vertices via Software, D3DCREATE_SOFTWARE_VERTEXPROCESSING, isto também para efeito de compatibilidade, porém o processamento de vertices via hardware, D3DCREATE_HARDWARE_VERTEXPROCESSING, consegue uma melhor performance, o que hoje em dia está se tornando muito comum devido às placas robustas que estão no Mercado.
Terminamos passando os ponteiros que apontam para nossos parâmetros de apresentação e para o dispositivo que está sendo criado, caso tudo corra bem nosso dispositivo estará criado e pronto para ser utilizado.

 

  

   if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,                   hWnd,D3DCREATE_SOFTWARE_VERTEXPROCESSING,&d3dpp,&g_pd3dDevice)))

   {

       return E_FAIL;

   }

 

   return S_OK;

}

 

Antes de continuar a implementação, vamos preparar nossa classe para que ela possa ser totalmente limpa da memória em seu encerramento, no nosso caso, ao final do jogo.

Com a função Cleanup() liberamos os recursos dos nosso dispositivo e do Direct3D que foram utilizados para nossa aplicação, mas aqui podemos centralizer outros recursos a serem “disposados”, é questão de evoluir o projeto.

 

VOID Cleanup()

{

    if(g_pd3dDevice != NULL)

        g_pd3dDevice->Release();

 

    if(g_pD3D != NULL)

        g_pD3D->Release();

}

 

 

O método Release elimina da memória os nossos objetos.
Agora podemos fazer o bloco de código mais cobiçado de um game, o Render().
É neste ponto que fazemos a renderização dos objetos de nossa cena, independentemente da forma como ele está implementado ou qual Engine utilizamos, aqui é onde tudo acontece.

 

VOID Render()

{

    if( NULL == g_pd3dDevice )

        return;

 

    //Preenche o nosso dispositivo com a cor preta

    g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0 );

   

    //A cena começa aqui

    if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )

    {

        //Aqui estaria a renderização dos objetos

   

        //Onde a cena termina

        g_pd3dDevice->EndScene();

    }

 

    //Apresenta a nossa cena

    g_pd3dDevice->Present( NULL, NULL, NULL, NULL );

}

 

Esta função é fundamental para entendermos como tudo funciona. Antes e qualquer coisa precisamos limpar a tela, isso garante que nenhum vestígio do ultimo “quadro” da cena atrapalhe a visualização da atual.
Neste ponto,
g_pd3dDevice->Clear, preenchemos o nosso backbuffer com alguma cor, no caso a preta, assim ele estará pronto para receber o desenho dos objetos da cena e depois poder fazer a troca para o frontbuffer.

 

Com a buffer preparado podemos iniciar a cena:

 

if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )

{

 

 

Obtendo sucesso na inicialização, teremos agora o dispositivo pronto e aceitando tudo que pintarmos nele, a partir deste ponto podemos verificar cada objeto que está visível e invocar o seu método de renderização, até que encerremos a cena.

g_pd3dDevice->EndScene();

 

 

Agora não podemos mais renderizar ninguém, se o fizermos não surtirá efeito algum a nível de visualização. O que podemos fazer então é apresentar isto para a tela, fazendo a troca de toda nossa imagem do back buffer para o front buffer.

 

 

g_pd3dDevice->Present( NULL, NULL, NULL, NULL );

 

Sem nenhuma opção adicional, simplesmente apresentamos o nosso quadro e encerramos a renderização que logo sera invocada novamente inúmeras vezes.
Antes de encerrar nossa classe com o ponto de entrada precisamos tartar as mensagens que chegam para ela das outras classes e do sistema operacional.

 

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

    switch(msg)

    {

        case WM_DESTROY:

            Cleanup();

            PostQuitMessage(0);

            return 0;

 

        case WM_PAINT:

            Render();

            ValidateRect(hWnd, NULL);

            return 0;

    }

 

    return DefWindowProc(hWnd, msg, wParam, lParam);

}

 

Com esta chamada à API do Windows podemos tartar as mensagens que chegam para nossa classe e assim deteminar comportamentos para ela.

Em nosso caso é interessante tratarmos apenas 2 mensagens por enquanto:

 

WM_DESTROY: É recebida quando tentamos fechar a janela, é o momento certo para liberarmos todos os recursos que estamos utilizando, para isto a função criada anteriomente Clenaup() existe.
Neste ponto também enviamos uma mensagem ao Sistema Operacional informando o encerramento da aplicação e daí em diante ele cuida do resto.

 WM_PAINT: Aciona nossa função de renderização, que faz todo o trabalho de pintar os objetos no dispositivo e então com ValidateRect fazemos a validação de toda a area de visualização da janela para que nosso dispositivo possa ser exibido corretamente.

 

Agora podemos definir o ponto de entrada da nossa aplicação, que é baseada nas janelas da API do Windows então devemos utilizar a função WinMain, vejam:

 

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )

{

    //Registra a classe de nossa janela

    WNDCLASSEX wc = {sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,

                      GetModuleHandle(NULL), NULL, NULL, NULL, NULL,

                      SHARP_CLASS_NAME, NULL};

    RegisterClassEx( &wc );

 

    //Cria a janela
    HWND hWnd = CreateWindow(
SHARP_CLASS_NAME, "Preparando o Dispositivo", WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,                             NULL, NULL, wc.hInstance, NULL );

 

    //Inicializa o Direct3D

    if(SUCCEEDED(InitD3D(hWnd)))

    {

        //Exibe a janela

        ShowWindow(hWnd, SW_SHOWDEFAULT);

        UpdateWindow(hWnd);

 

        //O loop principal

        MSG msg;

        while(GetMessage(&msg, NULL, 0, 0))

        {

            TranslateMessage(&msg);

            DispatchMessage(&msg);

        }

    }

 

    UnregisterClass(SHARP_CLASS_NAME, wc.hInstance );

    return 0;

}

 

A macro de retorno da função WinMain, INT, é equilente ao tipo de dados int, ela é seguida da instrução que indica ao compilador que é uma chamada à API do Windows, sua assinatura raramente é utilizada, porém é padrão.

 

Agora precisamos criar uma instância da estrutura WNDCLASSEX, que é responsável pelo estilo de nossa janela. Atente para o ponteiro que aponta para a nossa função MsgProc, ela será criada adiante e fará o tratamento das mensagens de nossa aplicação.

Notem que utilizamos a nossa macro SHARP_CLASS_NAME para associar nossa classe na estrutura.

Agora podemos registrar nossa classe com a função RegisterClassEx, passada como ponteiro para e assim fica pronta para retornar a instância de nossa janela.

 

 

Com a classe registrada, podemos criar a janela, faremos isso através da função CreateWindow. Para ela passamos novamente o nome de nossa classe , SHARP_CLASS_NAME, definimos o título da janela e seu estilo de bordas e botões(OVERLAPPEDWINDOW).

Também podemos definir aqui a posição(100,100) e dimensões da janela(300x300).

E por útlimo é válido observar a passagem da instância de nossa classe para a janela através da estrutura que acabamos de delclarar, wc.hInstance.

 

Utilizamos a função GetMessage para receber as mensagens ininterruptamente. As mensagens são traduzidas pela função TranslateMessage, que prepara os dados de forma que nossa aplicação possa entender e repassa-a para a função DispatchMessage que se encarrega de encaminhar a mensagem para a aplicação e assim acionar o nosso método Render caso seja necessário.

 

Quando recebermos uma mensagem do tipo WM_QUIT, o loop se encerrará e então precisamos avisar ao Sistema Operacional que a aplicação se encerra para ele cuidar dos “finalmentes” e assim desregistraoms nossa classe e chegamos ao final do programa.

 

O resultado é este:

 

image002.jpg 

Foi um árduo trabalho, concordo, porém nunca mais precisaremos reescrever este código, apenas adicionar alguns aditivos à ele, tendo em vista que ele é comum a qualquer game ou aplicação gráfica que seja desenvolvida com C++ e DirectX.
Espero que ajude vocês nesta longa e divertida caminhada.

 

Links úteis:
DirectX Resource Center: http://msdn.com/directx
Recursos e downloads de aplicativos para desenvolvimento de aplicações com DirectX e Xna

 

 

SharpGames http://www.sharpgames.net

Comunidade brasileira de desenvolvimento de jogos na plataforma Microsoft

 

Referências:

Creating a Device: http://msdn.microsoft.com/library/enus/directx9_c/

directx/graphics/tutorialsandsamples/tutorials/

direct3dtutorials/1/tutorial1.asp?frame=true

 

 

 

Livro Programação de Jogos com C++ e DirectX de André Santee