Introdução à programação de jogos e aplicativos 3D com DirectX para código gerenciado.

 

   Olá pessoal. Este é o primeiro de uma série de artigos sobre desenvolvimento de jogos usando o framework do DirectX. Quem curte jogos de computador com certeza alguma vez já se deparou com algum pedido de instalação ou atualização do DirectX para que o jogo pudesse funcionar. Isto acontece porque o jogo utiliza determinadas classes e métodos do framework que são necessários para o correto funcionamento do mesmo. Mas o que é um framework? É apenas um pacote de componentes, uma estrutura de suporte bem organizada e desenvolvida que servirá de base para outras aplicações. Você utiliza os componentes de um framework para desenvolver seus próprios aplicativos. O framework do DirectX é chamado de DirectX SDK (Software Development Kit [Obs: apenas para o sistema operacional Windows]) e pode ser obtido gratuitamente no site da Microsoft em http://msdn2.microsoft.com/en-us/xna/aa937788.aspx. A versão mais recente do mesmo até a data de publicação deste artigo é a de abril de 2007 (a que estou usando). Então sempre mantenha seu framework do DirectX (também não se esqueça de seu .NET Framework) atualizado para que os jogos mais recentes e suas aplicações DirectX possam ser executadas com a maxima eficiência de sua placa de vídeo. Mas como assim a máxima eficiência da placa de vídeo? É isso mesmo. O framework do DirectX é uma abstração da placa de vídeo. As APIs (Application Programming Interface [Interface De Programação De Aplicativos]) do DirectX nos dão classes e métodos essenciais para a manipulação destes dispositivos. São eles:

   DirectX Graphics
   – Direct3D
   – DirectDraw (Depreciado)
   DirectInput
   DirectPlay (Depreciado)
   DirectSound
   – DirectSound3D
   DirectMusic
   DirectSetup
   DirectX Media
   DirectX Media Objects

DirectX Graphics
——————
   – Direct3D
   ———–
      Um DDI (Device Driver Interface [Interface De Driver De Dispositivo]) para manipular e mostrar objetos tridimensionais. Nos dá uma interface para trabalharmos com programas 3D que podem utilizar qualquer recurso de aceleração gráfica externa instalada na máquina do usuário. Virtualmente todas as placas 3D para PCs (Personal Computers) suportam o Direct3D.
  
   – DirectDraw

   ————–
     Uma API 2D utilizada para aplicativos onde o alto desempenho é muito importante. Também utiliza aceleração de hardware mas não dá suporte para gráficos 3D. Você até pode utilizá-la para isto mas com certeza notará a perda de desempenho comparando-se ao Direct3D. Está API está depreciada e seu uso é desencorajado pela Microsoft, mas, muito programadores ainda a utilizam. A partir da versão 8.0 do DirectX, esta API foi incluida em um novo pacote chamado DirectX Graphics, que pode ser descrito como o Direct3D com adições do DirectDraw.

DirectInput
————-
   As entradas de nosso aplicativo se dão através desta interface (controles, teclado, mouse e etc).

DirectIPlay
————
   Para a comunicação on-line e em geral dos jogos. Também depreciada.

DirectSound
————–
   Uma interface entre a aplicação e a placa de som dando aos aplicativos a capacidade de reproduzir sons e músicas. Além de enviar informações para a placa de som, também possui funções como mixagem e controle de volume.

   – DirectSound3D
   ——————
      Interface para manipulação de sons tridimensionais.

DirectMusic
————-

   Para músicas e sons compostos no DirectMusic Producer. Um software de composição de conteúdo musical para profissionais da área que desejam compor audio para DirectX.

DirectSetup
————-

   Uma biblioteca simples que comanda a instalação do DirectX em sua máquina.

DirectX Media
—————

   Inclui componentes necessários para aplicações de audio do Windows que usam DirectAnimation, DirectShow e DirectX Transform. Estes componentes dão suporte a animação, media streams (transmissão de vídeo e audio pela Internet por exemplo) e interatividade.

DirectX Media Objects
————————
  
Suporte para objetos de streaming como codificadores, decodificadores e efeitos. 

   Como estaremos desenvolvendo aplicações 3D utilizaremos na maior parte do tempo o Direct3D para executarmos nossas tarefas. O Direct3D é um namespace um "pouquinho" grande onde estão armazenadas todas as classes que trabalham em baixo nível, especiais para manipularmos diretamente nossa placa de vídeo. Ele é composto de 44 classes, 52 structures e 47 enumerations (agora imaginem as ramificações de quase 150 membros).
   Como o namespace é grande e complexo, estudar DirectX não é coisa pra qualquer um e nada que se aprenda da noite para o dia. Exigirá de você muito estudo e esforço, pois, você estará lidando com funções que traballham direto com o hardware. Funções complexas com parâmetros complexos e que exigem um raciocínio forte e muita persistência. Também, grande parte dos tutoriais e e-Books na Internet estão em inglês, o que facilitará muito a busca de conhecimento para quem tem familiaridade com o ídioma, senão, hora de aprender. Você terá que se familiarizar com um mundo novo de conceitos e regras que abrangem o mundo 3D e que não estavam presentes no mundo 2D de antigamente. Não bastando, a matemática se torna também um dos fatores fundamentais para o desenvolvimento de jogos. Recomendo conhecimentos em geometria analítica, cálculo diferencial e integral 1, 2, 3 e se for possível (e louco o suficiente), 4 (podem ter certeza, criação de jogos é um investimento a longo prazo).
   Um destes novos conceitos do mundo 3D é um novo eixo em nosso plano cartesiano. Antigamente em aplicações 2D, podiamos desfrutar apenas de duas dimensões. São elas X e Y. O eixo X é representado por uma linha horizontal e o eixo Y por uma linha vertical. Através destas linhas e suas marcações numéricas, somos capazes de chegar a determinados lugares do plano, seguindo suas coordenadas, onde podemos marcar pontos e criar vértices, que por sua vez formarão figuras geométricas por exemplo (não é possível que você não fez um destes na escola).

   Esquerda, direita, cima, baixo e diagonais. Estas eram nossas possibilidades em jogos como Mario Bros., Donkey Kong, Super Metroid, Megaman X, Street Fighter e etc (também chamados de jogos de plataforma). Como vimos na figura acima, poderiamos criar por exemplo, um mecanismo de colisão na reta azul traçada no plano e fazer um objeto Moto ou Carro se movimentar sobre a mesma. Mas, e se não quiséssemos mais ir para frente ou para trás. Ao invés disso gostaríamos que nossa moto desse as costas para tela e seguisse em frente, rumo ao fundo do cenário (ou então imagine o Ken ou o Ryu dando as costas para o monitor da TV e seguir tela adentro para desviar de um "Alek Full" [rs]). Aí está a grande diferença de objetos 2D e 3D. Em ambientes 3D temos um terceiro eixo chamado Z (ou o eixo de fundo). Observe com bastante atenção a figura abaixo:

  
  
O que você nota de diferente nesse plano? Percebeu que as coordenadas X e Y estão "deitadas". E que nosso eixo Y deu lugar ao eixo Z. Mas não é bem assim. O que aconteceu de verdade é que nosso plano foi realmente "deitado" para que possamos enxergar com clareza a existência do eixo Z. Imagine o seguinte. Você é apenas a sua cabeça, mais nada. A ponta da seta do eixo Z é uma flecha que está apontada diretamente para seus olhos, pronta para te acertar, e seu nariz está na mesma direção do eixo Y. Concorda então que você está vendo diretamente a ponta da flecha (eixo Z), e que ela está bem de frente a você? Percebeu que olhando dessa perspectiva, os eixos X e Y voltam para os lugares corretos. O eixo Z é justamente isso. A distância que existe entre você (a câmera) e o objeto em questão. Quanto maior o valor de Z, mais distante o objeto está de você. Quanto menor o valor, mais perto.
   Agora que você entendeu os conceitos básicos de como funciona o ambiente 3D, mão à obra. Vamos nos arriscar nos nossos primeiros códigos DirectX.
   Para o nosso primeiro exemplo, faremos algo bastante simples. Apenas iniciaremos nosso dispositivo físico e apresentaremos uma tela limpa e vazia, da cor que você escolher. Para uma tela vazia e sem conteúdo, vocês verão que o código se torna relativamente médio e de dificuldade média também (para quem está começando). Os métodos utilizados também deverão ser todos novidade para você, caso ainda não tenha alguma prática em desenvolvimento com o framework do DirectX. Antes de postar todo o código, entenderemos os aspectos fundamentais de sua formação.
   Para começarmos, iremos adicionar as referências das bibliotecas do DirectX SDK instaladas no seu computador. Adicione Microsoft.DirectX e Microsoft.DirectX.Direct3D ao seu projeto/solução. Por enquanto precisaremos apenas destas. Referências ajustadas, podemos agora utilizar os dois namespaces necessários para construir nossa aplicação:

   using Microsoft.DirectX;

   using Microsoft.DirectX.Direct3D;

   Havíamos visto anteriormente que o framework do DirectX é uma abstração da placa de vídeo. Então como obtermos uma instância da mesma e começar a manipulá-la? Para isso, o namespace do Direct3D nos fornece a classe Device.
   A classe Device é o objeto pai de todos os objetos gráficos em uma cena. Ela executa renderizações (geometrias e desenhos) básicos, cria recursos, armazena variáveis de nível de sistema e obtém/ajusta paletas de cor. Criaremos uma classe que terá uma variável de instância chamada device do tipo Device. Compartilharemos o objeto Device criado em nossa classe através desta variável.
 
   Device device = null;

   Iniciamos a variável device como null porque ela ainda não aponta para nenhum objeto do tipo Device. Annn? Como assim aponta? Achei que eu tava livre disso! (rs) Quando você cria uma variável de tipo não primitivo (tipos primitivos são int, float e etc), isto é, uma variável de um tipo de objeto qualquer (Device por exemplo), o compilador cria o objeto na memória mas a variável que manipulamos (device) é apenas uma referência para o objeto Device recém criado. Assim podemos compartilhar este mesmo objeto com outras referências à um objeto Device e você não precisa se preocupar com a criação de ponteiros. Mas lembre-se que até aqui, device é apenas uma referência do tipo Device que está vazio. Ainda não existe nenhum objeto Device na memória e a variável device não faz referência a lugar algum.

   Uma dica de desempenho neste ponto, é que não há necessidade de inicializar o membro device como null. Quando criamos variáveis de instância, elas são implícitamente inicializadas pelo compilador, fazendo-se desnecessária a inicialização explícita dos mesmos. Variáveis do tipo objeto são automaticamente ajustadas para null, variáveis do tipo bool para false e numéricas para 0.
   O próximo passo para a criação de nosso objeto Device é entender a estrutura PresentParameters. Esta classe é muito importante, pois, definirá os parâmetros de apresentação da janela do nosso aplicativo. Neste exemplo mudaremos apenas duas propriedades desta classe. São elas Windowed e SwapEffect (mudamos essas duas propriedades porque executaremos nossa aplicação no modo de janela, e neste modo, a aplicação não tem total controle sobre o buffer [diferente do modo tela cheia], exigindo assim, certas particularidades). A primeira indica se nossa aplicação será executada em tela cheia ou em uma janela comum. Ajustaremos como true para que a segunda opção prevaleça, pois, ainda, não queremos um aplicativo em tela cheia. Aplicativos em tela cheia, manipulam diretamente a memória de vídeo. Por isso, podemos tomar vantagens exclusivas neste modo que não seriam eficientes no modo de janela. A segunda indica o "efeito de trocas" (swap effect) do programa. Para entendermos esta propriedade teremos que fugir um pouco dos códigos.
 
Rasterização
————–
 
   Rasterização (conversão de imagens descritas em gráficos vetoriais e etc para imagens no formato Raster) é uma técnica de renderização utilizada atualmente por todas as empresas fabricantes de placa de vídeo no mercado (Ray Casting, Radiosity e Ray Tracing são outros exemplos). Ela é mais rápida pois utiliza as primitivas 3D (triângulos e poligonos) para criar "espaços vazios". Assim ela consegue enxergar apenas os pixels que precisam ser modificados e interage apenas nos mesmos, diferente de uma renderização pixel por pixel que modifica todo o espaço da cena. Deste modo, o rasterizador pega um stream de vértices 3D e os transforma em suas respectivas coordenadas 2D criando assim uma imagem. Logo em seguida ele preenche os polígonos com suas cores apropriadas. É na a rasterização que nossa imagem é enviada ao back buffer (explicado logo abaixo). Também é importante lembrar que a conversão pixel por pixel frequentemente traz uma melhor qualidade de imagem, apesar do baixo desempenho. Agora apenas por curiosidade, vamos comparar duas imagens. São dois carros. Um renderizado por rasterização (New Beetle vermelho) e o outro renderizado por traçado de raios (Ray Tracing) (New Beetle azul). Veja como realmente a renderização faz a diferença. Note que a rasterização, apesar de muito pior graficamente, comparando-se ao traçado de raios, é utilizada por todas as fabricantes devido a seu alto desempenho. O problema do Ray Tracing é a complexidade de seus cálculos. Imagens podem demorar minutos, ou até horas para serem renderizadas. Mas a qualidade se torna impecável. Repare nos detalhes, reflexos e efeitos de luz.

                      
 
   Na IDF Spring de 2004, o professor de computação gráfica Phillip Slusallek da universidade de Saarland na Alemanha, demonstrou um sistema onde ele conseguia renderizar em tempo real, imagens 3D que utilizavam o algorítimo de Ray Tracing. Só que para isso, ele utilizou uma "singela" configuração: 22 racks, cada um contendo 2 processadores Xeon de 2,2 Ghz. O sistema tinha uma capacidade total de processamento de 400 Giga FLOPS.

Double buffering e page flipping
———————————–
 
   Double Buffering ou Ping-Pong Buffering é uma técnica utilizada para reduzir ou remover "vestígios" durante o processo de desenho. Mas como assim? A maioria dos monitores utilizam a frequência de 60 Hz para redesenho. Isto é, a tela é redesenhada cerca de 60 vezes por segundo. Durante estas "redesenhadas", operações mais complexas (como criação de movimento por exemplo) podem ser enviadas ao monitor antes que o processo gráfico das mesmas termine completamente, causando assim, efeitos indesejáveis ao jogo como cortes e algumas "piscadas". 
   Entender a lógica do double buffer é bastante simples. Vamos nos referir ao seu monitor como sendo o primary surface (mais chamado de front buffer, uma região na memória para armazenar as imagens). Abaixo desta, outro espaço de memória em "off" chamado back buffer. É no back buffer que estão as imagens já prontas para serem enviadas para o front buffer (processo chamado blit ou blitting, escreve-se blt [Block Line Transfer]) evitando assim, milhares de acessos consecutivos a memória. 

   Podemos utilizar mais de um back buffer se quisermos para aumentarmos o desempenho da aplicação. Suponha que tenhamos agora mais um back buffer (agora dois) e também um ponteiro que chamaremos de video pointer. Agora imagine que se, ao invés de fazermos um blt, apenas trocarmos os ponteiros de vídeo entre os back buffers 1 e 2. Assim, no próximo refresh, a placa de vídeo apenas usará a imagem para qual o video pointer está apontando, e enquanto isso, o outro back buffer já estará se preparando para enviar sua surface para o front buffer quando este mesmo apontar para ele. Esse processo é chamado de page flipping (e essa corrente [chain] de trocas [swaps] de back buffers é chamada de swap chain).
   Não se esqueça que a quantidade de back buffers é medida de acordo com a quantidade de memória disponível na placa de vídeo.
   
   Agora que entendemos alguns conceitos básicos, vamos voltar para a propriedade SwapEffect e seus parâmetros que, por sua vez, manipulam o efeito de troca de nosso aplicativo através de uma enumeration composta de três membros: Copy, Flip e Discard. Vamos mais a fundo nessa questão.
   O membro Copy, pode ser especificado apenas para swap chains que utilizam apenas um back buffer. Não importa se a aplicação está em tela cheia ou rodando em janela comum. O runtime garante que a semântica utilizada no método Present sempre seja baseada na cópia. Mais especificamente, este membro deixa o conteúdo do back buffer inalterado (ao invés de preenche-lo com o conteúdo do front buffer, como em Flip) copiando seu conteúdo direto para janela alvo. Tenha cuidado ao utilizar esta opção no modo janela pois, nenhuma tentativa de sincronizar a cópia com o tempo de vertical retrace do monitor será feito, podendo assim, causar efeitos indesejáveis como "rasgados" por exemplo. Se a utilizarmos em modo full screen, o runtime utilizará uma combinação de Copy e Flip que podem utilizar back buffers escondidos para realizar corretamente o método Present (libera a fila de back buffers) que por sua vez, é sincronizado com o vertical retrace do monitor. Não se esqueça que uma swap chain sinalizada com a flag Immediate é a única exceção. Neste caso, Present copia o conteúdo do back buffer diretamente para o front buffer sem esperar por vertical retraces. Uma exigência para o uso de Copy, é que a propriedade BackBufferCount seja igual a 0 ou 1 (0 equivale a 1 em BackBufferCount [já definidos por padrão]).
   Outra opção, CopyVSync, se comporta exatamente como o SwapEffect Copy, exceto que ele sincroniza sua cópia com o vertical retrace do monitor.
   O membro Flip, pode incluir múltiplos back buffers em uma fila circular (incluindo o front buffer) numerada de 0 a (n – 1 [onde n é a quantidade de back buffers]) sendo 0, o último buffer apresentado. Quando a "roda gira", o front buffer se torna o back buffer (n – 1) , e o back buffer 0 se torna o novo front buffer. Vale lembrar que o presentation rate de um full screen swap chain é determinado pela propriedade Caps.PresentationInterval quando um Device ou uma swap chain é criada. Já em modo janela, o flipping é implementado através de Copy.
   Quando utilizamos os métodos Copy e Flip, o runtime garante que o método Present não afetará o conteúdo de nenhum dos back buffers. Entretando, com esta garantia, podemos consumir uma quantidade significativa de memória de vídeo. Para evitar certos overheads, especialmente quando implementamos semânticas de flip para um swap chain em modo de janela, escolhemos a opção Discard que deixa para a placa, a escolha da melhor técnica de swap chain a ser utilizada.
 
   PresentParameters parametros = new PresentParameters();
   parametros.Windowed = true;
   parametros.SwapEffect = SwapEffect.Discard;
 
   Por enquanto ainda não darei mais detalhes sobre esta opção. O importante é saber que agora podemos instanciar o dispositivo do Direct3D utilizando a classe Device.
 
   device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, parametros);
 
   Agora sim inicializamos nossa variável de instância com um ponteiro válido para um objeto do tipo Device. O primeiro parâmetro, 0, indica a placa de vídeo a ser utilizada. O valor 0 indica o dispositivo padrão (seguido de 1, 2 e etc, caso outras placas estejam disponíveis).
   DeviceType.Hardware é uma enumeração que especifica o tipo de rasterização do Device que estamos usando. Escolhemos Hardware pois, estamos trabalhando com um dispositivo físico e gostaríamos de toda a sua aceleração. Software, como o próprio nome diz, rasterização por software (podendo também ser proprietário ou de terceiros), que traz baixo desempenho. Reference indica que estaremos usando recursos de software da biblioteca do Direct3D, entretanto, o rasterizador utilizará a CPU sempre que possível. NullReference como próprio nome indica é uma referência nula.
   O terceiro parâmetro, this, indica o controle onde a renderização irá acontecer. No nosso caso, o próprio formulário.
   Em seguida, temos um enumeration chamado CreateFlags que define algumas flags para a criação do nosso Device. Como essa enumeration é composta de muitos membros, vamos explicar apenas o valor selecionado e mais dois valores afins. Uma definição mais específica da enumeração pode ser encontrada em http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_m_dec_2004/directx/ref/ns/microsoft.directx.direct3d/e/createflags/createflags.asp. Para uma descrição mais detalhada de cada membro, utilize a busca com o nome de cada valor (muitos métodos tem nomes iguais e classes diferentes [métodos abstratos e virtuais]. Por isso tome cuidado ao ler a definição de alguma classe, método e etc. Certifique-se que o caminho do namespace [Microsoft.DirectX.Direct3D por exemplo] está correto). Para entender o valor SoftwareVertexProcessing, temos que entender primeiramente o que é um vertex (Latim: plural vertexes ou vertices), ou simplesmente um vértice. Não passa de um ponto no espaço 3D. A unidade básica. Um ponto que está em qualquer lugar do mundo e tem as coordenadas X, Y e Z. Os vertexes são utilizados para a formação dos polígonos necessários para a rasterização de nossa cena 3D. SoftwareVertexProcessing, HardwareVertexProcessing e MixedVertexProcessing afetam justamente o processamento de nossos vértices. Estas três constantes são exclusivas e pelo menos uma delas deve ser chamada na criação do objeto Device podendo elas, ser combinadas com outras flags usando-se o operador bitwise OR (|). Como queremos processamento de vértices feito por software, escolhemos SoftwareVertexProcessing (não podemos mais mudá-lo). Se quiséssemos processamento por hardware, escolheriamos HardwareVertexProcessing (não poderíamos mais mudá-lo) e se quiséssemos processamento de vértices por ambos, hardware e software, escolheriamos MixedVertexProcessing, que nos dá a capacidade de troca entre esses dois modos de processamento. Mas porque SoftwareVertexProcessing? Por que apenas queremos garantir que nosso exemplo execute em todos os computadores, tendo eles uma placa de vídeo ou não. Como esse é nosso primeiro tutorial, não precisaremos utilizar recursos mais avançados, que exigem tratamento de erros e exceções.
   O nosso último argumento é o objeto PresentParameters criado recentemente. Passamos para nosso dispositivo os parâmetros de apresentação da tela definidos anteriormente.
   Colocaremos a criação do nosso dispositivo dentro de um bloco try/catch para que possamos parar mais elegantemente nossa aplicação em caso de erro na inicialização do nosso objeto. Utilizaremos a exceção DirectXException que é o pai de todas as exceções do DirectX. Caso nosso método seja inicializado com sucesso, retornamos true, senão, false.


PresentParameters parametros = new PresentParameters();

parametros.Windowed = true;

parametros.SwapEffect = SwapEffect.Discard;

device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, parametros);

   Apenas um bloco de nosso código final pois, teremos que criar agora, o método que limpará a nossa cena com a cor selecionada. Chamaremos esse método de Render pois, como o próprio nome diz, será o responsavel pela inicialização do processo de renderização. 


device.Clear(ClearFlags.Target, Color.Black, 1.0f, 1);

device.BeginScene();

// Aplicar renderizações 3D aqui.

device.EndScene();

device.Present();

   Parece complicado mas não é. O método Clear da classe Device irá limpar a tela para que possamos inicializar nossa aplicação 3D. Ela também pode fazer isso através de retângulos de tamanhos diferentes setados pelo programador em determinados pontos na tela, sempre com uma cor de fundo no padrão RGBA. O primeiro parâmetro deve ser um valor da enumeration ClearFlags que limpará o buffer do tipo escolhido. Se escolhermos Stencil (muito utilizado em áreas de mascaramento, onde certas regiões são proibidas a certos objetos), estaremos limpando todos os dados de tipo stencil de nosso Device. Se escolhermos ZBuffer estaremos especificando um buffer que usa um valor de fundo (Z) para cada pixel na cena. Lembre-se que pixels com z-value pequeno sobrescrevem pixels com z-values mais altos. Escolhemos a opção Target pois, esta especifica uma superfície renderizada. Assim estamos indicando o "alvo" (target) de nossa renderização. O segundo parâmetro apenas define a cor de nossa tela utilizando a classe Color (utilizamos preto [Black], mas fique à vontade para escolher sua cor). O terceiro parâmetro representa um novo valor float para o zdepth. Esse valor pode variar entre 0.0 e 1.0 onde 0.0 representa a distância mais próxima do observador. O quarto e último parâmetro limpa o stencil buffer para o valor especificado.
   Logo em seguida, o método EndScene sempre deverá ser chamado se o método BeginScene for utilizado. É entre estes dois métodos que definimos o ambiente antes de liberarmos nossa fila de back buffers com o método Present. Aqui definimos diversas renderizações. O método Present então, preenche o conteúdo da tela com a sua sequência respectiva de back buffers (detalhe, este método não pode ser chamado entre os métodos BeginScene e EndScene, a menos que o alvo da renderização não seja a renderização atual).
   Vamos postar agora o código que compõe nossa partial class e inicializa nosso ambiente 3D. Para este exemplo, criaremos um evento OnPaint que chamará o método Render assim que o evento for disparado no início da execução.

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using Microsoft.DirectX;

using Microsoft.DirectX.Direct3D;

namespace Inicializando_O_Direct3D

{

public partial class Form1 : Form

{

//==============================================================================

// Variável de instância da classe.

Device device = null;

//==============================================================================

// Função criada pelo Visual Studio 2005.

public Form1()

{

InitializeComponent();

}

//==============================================================================

public bool InicializaGraficos()

{

try

{

PresentParameters parametros = new PresentParameters();

parametros.Windowed = true;

parametros.SwapEffect = SwapEffect.Discard;

device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, parametros);

return true;

}

catch (DirectXException)

{

return false;

}

}

//==============================================================================

public void Render()

{

// Se o Device não for criado, termina o método.

if (device == null)

return;

device.Clear(ClearFlags.Target, Color.Black, 1.0f, 1);

device.BeginScene();

// Aplicar renderizações 3D aqui.

device.EndScene();

device.Present();

}

//==============================================================================

private void Form1_Paint(object sender, PaintEventArgs e)

{

Render();

}

//==============================================================================

}

}

   Agora vamos ao nosso método Main.

using System;

using System.Collections.Generic;

using System.Windows.Forms;

namespace Inicializando_O_Direct3D

{

static class Program

{

///

/// The main entry point for the application.

///

[STAThread]

static void Main()

{

Application.EnableVisualStyles();

Application.SetCompatibleTextRenderingDefault(false);

// Usamos o bloco using para que o objeto DeviceForm exista apenas neste escopo.

using (Form1 DeviceForm = new Form1())

{

// Se InicializaGraficos() retornar false.

if (!DeviceForm.InicializaGraficos())

{

MessageBox.Show("Não pode inicializar o Direct3D.",

"Erro!",

MessageBoxButtons.OK,

MessageBoxIcon.Error);

return;

}

// Garantimos que o formulário exista para que o ponteiro this

// sempre aponte para o formulário.

DeviceForm.Show();

// Roda a aplicação.

Application.Run(DeviceForm);

}

}

}

}
 

   Depois de um pouquinho de trabalho, temos então nosso tão satisfatório resultado. 

    Bom, o resultado pode não ser lá tão satisfatório como vocês imaginavam, mas infelizmente este é o começo (poxa, mas é só uma telinha preta???). Como havia citado anteriormente e reforçando mais uma vez, programação pra jogos é um investimento a longo prazo. Já pararam pra pensar que um jogo, é um software que utiliza quase todas (senão todas) as áreas da ciência da computação. Vamos desde a física até os pontos mais baixos da programação, sem contar os designs gráficos, hardware, matemática extremamente complexa, criação de sons e filmes computacionais e mais uma série de áreas específicas que exigem profissionais técnicos extremamente qualificados. É um aplicativo muito mais complexo do que a grande maioria dos softwares comerciais disponíveis no mercado. Por isso, a partir de hoje pense duas vezes antes de falar que um jogo original (lançamentos variam entre R$ 100,00 e R$ 200,00) é caro. É justamente o oposto se formos comparar a sua complexidade ao preço. Como dizia um amigo: "Nós é que ainda não temos dinheiro o suficiente". 🙂
   Espero que este artigo tenha sido útil para você, principalmente se você é um amante de jogos como eu, e sonha um dia, trabalhar nessa magnífica área que com certeza é para muito poucos (e muitíssimo poucos). Bons estudos e um grande abraço a todos!

This entry was posted in C# .NET (DirectX). Bookmark the permalink.

Leave a comment