GameDev #7: Conceitos de Programação de Jogos no XNA

em 01/03/2010

Nos nossos últimos dois encontros aqui na coluna GameDev do Nintendo Blast conhecemos melhor o XNA, suas camadas, vantagens e possibilida... (por Jones Oliveira em 01/03/2010, via Nintendo Blast)

Nos nossos últimos dois encontros aqui na coluna GameDev do Nintendo Blast conhecemos melhor o XNA, suas camadas, vantagens e possibilidades. A partir de hoje aliaremos a teoria à prática e começaremos a utilizar as ferramentas do XNA, bem como escrever e, principalmente, compreender as linhas de código. Apesar de tomar “ares” de tutorial, não deixaremos de abordar e explicar os aspectos técnicos sobre o desenvolvimento de um jogo com XNA em nossa coluna.

Na coluna de hoje iremos criar um projeto XNA em branco, analisar sua estrutura e entender os conceitos básicos por detrás do jogo.

Para acompanhar a coluna de hoje e aproveitá-la em sua magnitude, precisaremos fazer o download e instalar a última versão do XNA Game Studio e o Microsoft Visual C# Express Edition, ambos gratuitos, que podem ser encontrados na sessão de downloads do XNA Creators Club (http://creators.xna.com/en-US/downloads). Após feito o download dos dois, instale primeiramente o Visual C# Express Edition e logo em seguida o XNA Game Studio. Ao longo das próximas colunas eu utilizarei uma versão diferente do Visual C# que na verdade é o Visual Studio Team System 2008 (versão paga e licensiada) que, para a nossa coluna, faz as mesmas coisas.

cco_dwnld_visualc2008 cco_resources_downloads_downloadItemImage_XNAGS3.1

 

O primeiro contato com o Visual Studio

Após as devidas instalações, abra o seu Visual C# Express Edition e uma janela parecida com a Imagem 1 deverá aparecer. Clique em File->New->Project (ou aperte Ctrl+Shift+N) para criar um novo projeto. Na janela que aparecer, selecione a opção XNA Game Studio 3.1 e em templates selecione Windows Game (3.1) (Imagem 2).

imagem1
Imagem 1

No campo name você definirá qual o nome desse seu projeto e em Location o nome da pasta aonde seu projeto ficará salvo - escolha qualquer nome e a pasta que você quiser para isso e dê OK.

imagem2
Imagem 2

Após o projeto ter sido criado, aperte F5 no seu teclado para compila-lo e colocar o jogo para funcionar. É claro que não aparecerá jogo nenhum – ao invés disso aparecerá uma entediante tela azul que indica que você está pronto para desenvolver o seu primeiro jogo com XNA. Feche a janela azul e volte ao Visual C# Express Edition e perceba que, à direita, a aba do Solution Explorer foi “povoada” por vários arquivos e pastas (Imagem 3).

 imagem3
Imagem 3

Perceba que com os arquivos de imagem Game.ico e GameThumbnail.png, seu projeto foi criado com dois arquivos de extensão .cs (Program.cs e Game1.cs) que são arquivos de código C#. Também foi criada uma pasta chamada Content que armazenará todo o conteúdo do jogo, tais quais sons, imagens, modelos 3D, texturas e porai vai.

Antes de começarmos a entender a estrutura dos arquivos de código do XNA, vamos recapitular um pouco a estrutura básica de um jogo.

 

Estrutura genérica de um jogo

No nosso último encontro apresentamos um fluxograma básico sobre como um jogo funciona e citamos que uma vez dentro dele, este fica “dando voltas” dentro de si próprio verificando se cumprimos todos os objetivos para alcançarmos o final e, finalmente, sairmos do jogo de uma vez por todas. Agora teremos essa visão de um lado mais técnico da coisa.

image[8]

A lógica central de qualquer jogo inclui a preparação do ambiente onde ele será executado, a execução do jogo em um loop até que o critério de fim de jogo seja alcançado e, finalmente, a limpeza desse ambiente. Se formos representar tecnicamente essa estrutura, podemos construir o seguinte pseudocógido:

  • Inicialização dos gráficos, dispositivos de entrada de dados e som
  • Carregar os recursos
  • Iniciar o loop do jogo. A cada loop:
  • Capturar os comandos do jogador
  • Executar os cálculos necessários (IA, movimentos, detecção de colisões, etc)
  • Testar se o critério de fim de jogo foi alançado – em caso positivo, o loop para
  • Renderizar gráficos em tela, gerar sons e respostas aos comandos do jogador
  • Finalizar os gráficos, dispositivos de entrada e som
  • Liberar os recursos (placa de vídeo, memória e CPU)

Claro, essa é uma visão bastante simplista, mas que já nos dá uma boa visão de como devemos “construir” o código do nosso jogo. Antes do XNA, os desenvolvedores não se preocupavam somente com essa estrutura, mas também em como tudo isso iria funcionar – todos os mínimos detalhes que não faziam parte do projeto de desenvolver o jogo em si deveriam ser pensados também.

Se voltarmos ao Visual C# Express Edition e observarmos atentamente os arquivos Game1.cs e Program.cs, perceberemos que eles apresentam métodos que nos permite afirmar que a estrutura dos jogos em XNA é bastante similar a estrutura do pseudocódigo que mostramos anteriormente. Sem entrar em maiores detalhes por enquanto, temos o seguinte pseudocódigo nos dois arquivos:

  • Game1() – Inicialização geral (Game1.cs)
  • Initialize() – Inicialização do jogo (Game1.cs)
  • LoadContent() – Inicializa e carrega recursos gráficos (Game1.cs)
  • Run() – Inicia o loop do jogo (Program.cs). A cada loop:
  • Update() – Captura os comandos do jogador, realiza cálculos e testa o critério de fim de jogo (Game1.cs)
  • Draw () – Renderiza os gráficos em tela (Game1.cs)
  • UnloadContent() – Libera os recursos gráficos

Dica: Nos arquivos .cs do seu projeto, o XNA gera a estrutura básica do jogo e comenta cada um desses métodos. Os comentários, que estão entre as tags <summary>, especificam melhor o que cada um faz.

Agora analisemos mais detalhadamente os métodos do arquivo Game1.cs.

 

A inicialização do jogo

Observemos o seguinte pedaço de código:

public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

Nele podemos perceber que a classe Game1 está sendo definida e criada, tendo como classe pai a Microsoft.Xna.Framework.Game, que nos permitirá trabalhar com gráficos, sons, controles e cálculos de lógica para o nosso jogo.

Logo em seguida definimos e inicializamos dois objetos do tipo GraphicsDeviceManager e SpriteBatch – esse nos permitirá desenhar textos e imagens 2D na tela do nosso jogo, enquanto aquele nos dará acesso aos recursos do gerenciador de dispositivos gráficos para que trabalhemos com os gráficos do nosso jogo.

Além disso, ainda temos a definição da pasta raíz aonde ficará todo o conteúdo do nosso jogo – isso é importante pois é a partir daqui que teremos acesso ao gerenciador de conteúdo do nosso jogo.

Observação: daqui para frente entraremos em um linguajar mais técnico da coisa. Sempre que possível, tentarei esmiuçar o palavreado e explicar da forma mais simples possível. Entretanto, é importante que haja um conhecimento básico sobre programação e o paradigma de Orientação a Objetos. Caso se interessem pelo assunto de verdade, aconselho o download e leitura de algumas apostilas de algoritmos, técnicas de programação e orientação a objetos.

 

O Graphics Device Manager (gerenciador de dispositivo gráfico)

game_developer

Como seu nome sugere, o gerenciador de dispositivo gráfico nos permitirá lidar diretamente com a camada gráfica do nosso dispositivo. Ele inclui métodos, propriedades e eventos que nos permite consultar e alterar essa camada. Trocando em miúdos, é o gerenciador que nos dará acesso aos recursos da nossa placa de vídeo.

Mais uma vez, antes do XNA havia a preocupação de programar a placa de vídeo para que essa trabalhasse corretamente com o jogo. Com a classe GraphicsDeviceManager do XNA, toda a preocupação, complexidade e detalhes de se trabalhar com uma placa de vídeo se esvai. Por ora, o que precisamos ter em mente é que sempre utilizaremos o objeto criado a partir da classe GraphicsDeviceManager para realizar qualquer operação gráfica.

 

O Content Pipeline (gerenciador de conteúdo)

O gerenciador de conteúdo é um dos recursos mais interessantes do XNA. A forma com que foi desenvolvido permite que importemos qualquer tipo de conteúdo de ferramentas diferentes para o nosso jogo. Em jogos que não utilizam XNA os desenvolvedores precisam se preocupar em como carregar tais conteúdos, aonde esse conteúdo está armazenado, se existem as bibliotecas corretas para importação e execução e mais uma série de dores de cabeça desnecessárias.

O gerenciador de conteúdo do XNA dá conta de todo o recado, fazendo com que esse doloroso processo se torne bastante agradável. Basicamente o que ele faz é importar os conteúdos, processá-los com o compilador de conteúdo e gerar um arquivo de conteúdo (extensão .XNB) que será utilizado pelo jogo.

xna_content_pipeline

O mais interessante é que, apesar de abranger uma enorme quantidade de formato de arquivos, você ainda poderá desenvolver seus próprios compiladores para trabalhar com um conteúdo específico por alguma ferramenta que o XNA ainda não dê suporte (inclusive já existe uma boa biblioteca disso no XNA Creators Club – http://creators.xna.com). Discutiremos mais sobre isso em outros encontros.

 

Os métodos de inicialização do jogo

Se voltarmos um pouco ao começo desse artigo e analisarmos o pseudocódigo que contém os métodos gerados e utilizados pelo XNA, perceberemos que temos duas inicializações do jogo e que cujos propósitos ainda não foram devidamente explicados. Antes de começarmos a trabalhar no nosso jogo, precisamos entender o porquê dessas duas rotinas de inicialização.

O método Initialize() é chamado apenas uma vez quando o método Run() (que inicia o loop do jogo) é executado. É no método Initialize() que devemos trabalhar as rotinas de inicialização de componentes não-gráficos do nosso jogo, como por exemplo preparar o conteúdo de áudio.

Koenigsegg_28_3840 Os gráficos precisam ser carregados em um método diferente porque às vezes o jogo precisa recarregá-los. Para que o jogo rode em ambiente ótimo, os gráficos são carregados de acordo com as configurações da sua placa de vídeo. Quando as configurações da placa são alteradas, a resolução mudada, ou coisas desse tipo, os gráficos precisam ser recarregados. Como o método Initialize() é chamado apenas uma vez, antes do loop do jogo ser iniciado, os gráficos são inicializados no método LoadContent(), que é chamada toda vez que o jogo precisar carregar ou recarregar os gráficos.

 

O loop do jogo

winifredphillips_studio2 A maior parte do processamento do jogo acontece dentro do seu loop. É no loop que o jogo verifica os comandos do jogador, processa-os e dá o feedback no personagem do jogo, calcula a inteligência artificial, os movimentos são calculados e executados, as colisões entre objetos detectadas, a vibração do controle é ativada, o som é tocado, os gráficos desenhados na tela e os critérios para alcançar o fim do jogo checados. Enfim, praticamente tudo ocorre no loop do jogo.

No XNA, o loop do jogo depende de dois métodos derivados da classe Microsoft.Xna.Framework.Game: Update() e Draw(). No Update() são incluidos todos os cálculos do jogo, enquanto no Draw() você desenha os componentes do jogo em tela. Para compreender melhor, observemos os dois métodos:

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)
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            base.Draw(gameTime);
        }

Em relação as demais classes que já vimos, o que primeiro podemos denotar é que tanto o método Update(), quanto o Draw() recebem o parâmetro gameTime. No momento, o que precisamos saber é que esse parâmetro é crucial para a lógica do jogo. A partir dele é que o nosso jogo saberá quanto tempo passou desde que o último loop foi executado. Isso é extremamente importante para que possamos fazer os cálculos corretos – por exemplo, calcular a posição correta dos componentes do jogo de acordo com suas velocidades no jogo. Sem esse parâmetro, seria impossível organizar e desenvolver o jogo.

Outra coisa a ser observada é que, no método Update(), há um código pré-definido para sair do jogo quando se aperta o botão “Back” no controle do Xbox 360:

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

xbox-360-controller A classe GamePad nos permite acessar o controle e permite ativar a vibração dele. A captura dos comandos dados pelo usuário é em tempo real e não há nenhum atraso quanto a isso. Se observarmos atentamente, podemos ver que o trabalho com a interface do controle é bem simples e intuitiva, pois as propriedades são auto-explicativas:

  • GamePad: classe de acesso ao controle
  • GetState: pegue o estado do controle
  • PlayerIndex.One: apenas do primeiro controle
  • Buttons: acessa os botões do controle
  • Back: especificamente o “Back”
  • ButtonState.Pressed: quando o botão for pressionado
  • this.Exit(): sair do jogo

Logo, sem nenhuma dificuldade, podemos dizer que esse código está dizendo ao jogo que “quando o botão Back do controle 1 for pressionado, saia do jogo”. Simples, não?

Já o método Draw() inclui uma linha para limpar o dispositivo gráfico e preencher a tela do jogo com uma única cor – CornflowerBlue:

graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

Como falamos anteriormente, o gerenciador de dispositivo gráfico (representado aqui pela variável graphichs) é o nosso principal meio de acesso às propriedades do nosso dispositivo gráfico e suas propriedades. Nesse exemplo, em específico, utilizamos a propriedade GraphicsDevice, que expõem as propriedades e métodos que nos permite ler e configurar os vários detalhes da renderização do nosso jogo.

blue_screen_XNA

 

A finalização do jogo

Se formos fazer uma analogia bem rasteira de como as coisas são, podemos comparar desenvolver um jogo em XNA como a organização de uma festa. Para realizarmos uma festa de sucesso, primeiro temos que planejá-la e preparar o ambiente em que a festa será realizada. Ao término da festa, precisamos limpar o ambiente para “devolvê-lo” nas condições que o pegamos. E assim acontece com o XNA também.

Vimos que antes de começarmos o jogo em si, precisamos preparar o ambiente em que o jogo será executado (classes Game1(), Initialize() e LoadContent()), para começarmos a festa (métodos Run(), Update() e Draw()). Ao término do jogo, também precisamos dar uma geral no ambiente. Precisamos esvaziar a memória da plataforma em que o jogo está sendo executado e tudo o mais. O grande diferencial aqui está no método UnloadContent() do XNA.

É como se tivesse uma pessoa única e exclusiva que executa a faxina no nosso sistema ao término do jogo. O UnloadContent() possui o “coletor de lixo” (garbage collector) oriundo do .NET Framework que simplifica bastante o desenvolvimento e execução de rotinas de encerramento em jogos XNA. E, o melhor de tudo, ele não atua somente quando o jogo é finalizado, mas também enquanto está em execução. Dessa forma, não precisamos nos preocupar muito com essa coleta de lixo dentro do jogo, a não ser que passemos a desenvolver jogos realmente complexos. Mas isso é assunto para depois.

 

Hoje tivemos o nosso primeiro contato com o Visual C# Express Edition e com a estrutura de um jogo em XNA. Podemos sentir de perto como as coisas funcionam e já temos consolidadas as bases para começarmos a desenvolver o nosso jogo em XNA. No nosso próximo encontro iremos ver como funciona o esquema de gráficos 2D no XNA, desenharemos nossos primeiros elementos em tela, daremos vida a eles e os faremos colidir um com os outros. Caso tenham alguma dúvida quanto ao encontro de hoje, tire-a e participe nos comentários. Fiquem atentos que o nosso próximo encontro promete. Até lá!


Escreve para o Nintendo Blast sob a licença Creative Commons BY-SA 3.0. Você pode usar e compartilhar este conteúdo desde que credite o autor e veículo original.
Este texto não representa a opinião do Nintendo Blast. Somos uma comunidade de gamers aberta às visões e experiências de cada autor. Escrevemos sob a licença Creative Commons BY-SA 3.0 - você pode usar e compartilhar este conteúdo desde que credite o autor e veículo original.