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:
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