Desenvolvendo em XNA

GameDev #12: Arregace as mangas, é hora de desenvolver seu primeiro jogo 2D

Nos últimos 11 artigos da coluna GameDev você teve uma visão geral do funcionamento da indústria, conheceu as principais responsabilidad... (por Sergio Oliveira em 16/04/2013, via Xbox Blast)


Nos últimos 11 artigos da coluna GameDev você teve uma visão geral do funcionamento da indústria, conheceu as principais responsabilidades de cada profissional envolvido na produção de um jogo e aprendeu os conceitos básicos da programação de games para PC e Xbox 360 na plataforma XNA da Microsoft. Caso se sinta preparado, então arregace as mangas, pois os próximos dois artigos ensinarão você a aplicar os conceitos aprendidos em um jogo 2D de verdade!
Nunca é demais lembrar que para executar o que é desenvolvido nesta coluna, é necessária a instalação do Microsoft Visual C# 2010 Express e do XNA Game Studio, ambos gratuitos.

Planejando seu primeiro jogo

Até aqui você já adquiriu certa prática com a forma que o XNA funciona e tem uma noção mínima de como trabalhar com ele. É chegada a hora de expandir os horizontes e desenvolver seu primeiro game. Apesar de simples, dá para garantir que ele será divertido.
O jogo que será criado aqui é baseado na proposta dos autores Evangelista, Lobão, Grootjans e Farias na obra Beginning XNA 3.0 Game Programming – From Novice to Professional. A explicação, didática e anotações, no entanto, são de inteira responsabilidade do autor desta coluna.
Apesar de trivial, todo jogo precisa ser bem planejado. Como já foi discutido nas primeiras edições da coluna, analisar e planejar todo e qualquer projeto é essencial para que ele seja bem sucedido, atinja seus objetivos e cumpra as expectativas – e sabemos que você não quer fazer seu primeiro jogo de qualquer jeito, certo? 

Planejar significa descobrir quais questões devem ser respondidas antes de iniciar o desenvolvimento do projeto. Sendo assim, imaginemos um cenário hipotético para o jogo. 

Você é um explorador intergaláctico e está preso em um campo de asteroides. Por quanto tempo você resistirá à chuva de asteroides? Esse será o tema principal do game, um desafio frenético onde você precisará se esquivar dos obstáculos que cruzam a tela a todo momento. 

Certamente esse é um conceito de jogo bem antigo, mas os jogadores nunca se cansam de escapar de asteroides e meteoros. Quanto maior for o tempo que ele ficar sem ser atingido por um desses, mais pontos acumulará. Além disso, a quantidade de obstáculos aumenta à medida que o tempo passa, tornando o desafio cada vez mais difícil. 

É importante ter em mente as regras do jogo e seu funcionamento antes de começar a desenvolvê-lo - e nesse caso você já teve uma visão geral de como será:
  • O jogador poderá se mover livremente na tela e não ultrapassará os limites dela; 
  • Os asteroides aparecerão a partir do topo da tela e se moverão para baixo com ângulo e velocidade aleatórios. Depois de algum tempo, um novo asteroide será adicionado à tempestade; 
  • A pontuação será determinada pelo número de asteroides na tela; 
  • Se o jogador colidir com um asteroide, sua pontuação será zerada e o jogo irá reiniciar com a quantidade inicial de asteroides.
Detalhes como a quantidade inicial de asteroides, e quanto tempo levará para que outro asteroide surja na tela não são especificados pois eles são parâmetros do game, e não regras. 

Para ter ideia do que será trabalhado, ao término desses dois próximos artigos o jogo será exatamente igual ao da imagem abaixo.


O ponto de vista de um programador

Para um programador de jogos, coisas como a espaçonave, asteroides e pontuação são objetos no jogo – no sentido de objeto instanciado de uma classe. Você, como bom programador que é, deve detalhar esses objetos antes de começar a programar qualquer coisa. 

Cada objeto do jogo tem suas próprias características e comportamento. Os asteroides “caem” da tela, o jogador controla a espaçonave, a pontuação aumenta de acordo com a contagem de asteroides e assim por diante. A definição correta do comportamento dos objetos em um jogo é uma das tarefas mais desafiadoras durante o desenvolvimento, por isso também é importante ter em mente um game bem definido e planejado. 

Ainda é necessário pensar nos efeitos sonoros que serão utilizados. Este projeto incluirá apenas três sons: a música de fundo que tocará enquanto o jogo é executado, o som que tocará quando um novo asteroide for adicionado à tempestade e outro de explosão quando houver colisão entre a nave e um asteroide.

Iniciando e preparando o projeto

Como foi visto nos artigos anteriores, abra o Visual C# 2010 Express e crie um novo projeto XNA Windows Game. Atribua um nome nada sugestivo a esse jogo: Asteroides. Ao criar o projeto, seu Solution Explorer deverá estar igual ao da imagem abaixo:


Já observamos anteriormente que a pasta Content é especial nos projetos XNA. Nela você colocará todos os itens de conteúdo do jogo, como imagens, sons e tudo aquilo que será carregado no Content Pipeline.

Para facilitar o trabalho, você pode fazer o download de todos os arquivos utilizados nesse projeto, e que devem ser colocados na pasta Content, no link http://sdrv.ms/107q1kv

Desenhando o background

Para começar a desenvolver o jogo, você deve adicionar uma imagem de fundo a ele. Se o game acontece no espaço, nada melhor que o tema de uma galáxia. Adicione o arquivo SpaceBackground.dds à pasta Content.



Agora que a textura está na pasta Content do projeto, você precisa carregá-la na aplicação para que ela possa preencher a tela do jogo. Defina essa textura no código da classe Game1 do arquivo Game1.cs:

//Textura do plano de fundo
private Texture2D backgroundTexture;

Conforme aprendido nos artigos passados, agora você deve carregar essa textura e inicializar o objeto spriteBatch no método LoadContent:

//Cria um novo spritebatch, que pode ser utilizado para desenhar texturas
spriteBatch = new SpriteBatch(GraphicsDevice);
//Carrega todas as texturas
backgroundTexture = Content.Load<Texture2D>("SpaceBackground");

Agora sim o plano de fundo pode ser carregado. Para isso basta adicionar o seguinte código no método Draw da classe Game1:

//Desenha a textura do background
spriteBatch.Begin();
spriteBatch.Draw(backgroundTexture, new Rectangle(0, 0,
                graphics.GraphicsDevice.DisplayMode.Width,
                graphics.GraphicsDevice.DisplayMode.Height),
                Color.LightGray);
spriteBatch.End();

Execute o seu jogo apertando F5. Se tudo estiver correto, a seguinte tela deverá aparecer:

Criando os componentes do game

O jogador será representado por uma pequena espaçonave cujo controle é feito pelo teclado do computador. A imagem dessa espaçonave está no arquivo Asteroides.png. Adicione-o ao projeto da mesma forma que fez com o arquivo anterior. Perceba que esse arquivo contém tanto a espaçonave quanto os asteroides que o jogador deverá desviar. 

Assim como foi feito com a textura do plano de fundo, declare esta nova textura na classe Game1:

//Textura dos asteroides
private Texture2D asteroidsTexture;

Então carregue-a no método LoadContent imediatamente após a textura do plano de fundo:

asteroidsTexture = Content.Load<Texture2D>("Asteroides");

Agora crie uma nova classe que representará a espaçonave do jogador. Adicione outro GameComponent ao projeto e nomeie-o Ship.cs. O novo arquivo inclui uma classe derivada de GameComponent. Esse componente de jogo deve permanecer visível, por isso precisa ser desenhado.



Para inserir este elemento no jogo, primeiramente é preciso determinar que ele será derivado de DrawableGameComponent ao invés de GameComponent.

Altere a definição da sua classe de:

public class Ship : Microsoft.Xna.Framework.GameComponent

Para:

public class Ship : Microsoft.Xna.Framework.DrawableGameComponent

O componente copia a textura da região que contém a espaçonave, ou seja, uma área específica da imagem. Para conseguir fazer isso, você definirá as coordenadas da textura na imagem e as coordenadas na tela de onde ela será desenhada.

Nosso elemento ainda precisa se mover de acordo com os comandos recebidos no teclado do computador, além de permanecer nos limites da tela – ou seja, a espaçonave não pode simplesmente sumir da região da janela do jogo. 

Sendo assim, há duas coisas muito bem definidas até aqui:
  • No método Draw deve-se desenhar a porção de imagem da espaçonave na tela; 
  • No Update é preciso atualizar a posição da espaçonave de acordo com os comandos do teclado.
Abaixo segue o código completo dessa classe:

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Asteroides
{
    /// <summary>
    /// Esse é um componente de jogo que implementa a espaçonave do jogador
    /// </summary>
    public class Ship : Microsoft.Xna.Framework.DrawableGameComponent
    {
        protected Texture2D textura;
        protected Rectangle spriteRetangulo;
        protected Vector2 posicao;
        protected SpriteBatch sBatch;

        //Largura e altura do sprite na textura
        protected const int larguraNave = 30;
        protected const int alturaNave = 30;

        //Área da tela
        protected Rectangle limiteTela;

        public Ship(Game game, ref Texture2D aTextura) : base(game)
        {
            textura = aTextura;
            posicao = new Vector2();

            //Pega o spritebatch atual
            sBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            //Cria o retângulo fonte
            //Isso representa aonde o sprite está localizado
            spriteRetangulo = new Rectangle(31, 83, larguraNave, alturaNave);

            //Define o limite da tela do jogo
            limiteTela = new Rectangle(0, 0,
                Game.Window.ClientBounds.Width,
                Game.Window.ClientBounds.Height);
        }

        /// <summary>
        /// Coloca a nave na posição inicial na tela
        /// </summary>
        public void PutinStartPosition()
        {
            posicao.X = limiteTela.Width / 2;
            posicao.Y = limiteTela.Height - alturaNave;

            base.Initialize();
        }

        /// <summary>
        /// Atualiza a posição da espaçonave
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            //Move a nave de acordo com a entrada do teclado
            KeyboardState teclado = Keyboard.GetState();

            if (teclado.IsKeyDown(Keys.Up))
                posicao.Y -= 3;

            if (teclado.IsKeyDown(Keys.Down))
                posicao.Y += 3;

            if (teclado.IsKeyDown(Keys.Left))
                posicao.X -= 3;

            if (teclado.IsKeyDown(Keys.Right))
                posicao.X += 3;

            //Mantém a nave dentro dos limites da tela
            if (posicao.X < limiteTela.Left)
                posicao.X = limiteTela.Left;

            if (posicao.X > limiteTela.Width - larguraNave)
                posicao.X = limiteTela.Width - larguraNave;

            if (posicao.Y < limiteTela.Top)
                posicao.Y = limiteTela.Top;

            if (posicao.Y > limiteTela.Height - larguraNave)
                posicao.Y = limiteTela.Height - alturaNave;

            base.Update(gameTime);
        }

        /// <summary>
        /// Desenha o sprite da espaçonave
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            //Pega o spritebatch atual
            SpriteBatch sBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            //Desenha a espaçonave
            sBatch.Draw(textura, posicao, spriteRetangulo, Color.White);

            base.Draw(gameTime);
        }

        /// <summary>
        /// Pega o limite do retângulo da posição da nave na tela
        /// </summary>
        /// <returns></returns>
        public Rectangle GetBounds()
        {
            return new Rectangle((int)posicao.X, (int)posicao.Y, larguraNave, alturaNave);
        }
    }
}

Perceba que o método Draw não cria um novo SpriteBatch, como quando a textura do plano de fundo foi desenhada. Na verdade, o ideal é que não acrescente e destrua esses objetos muitas vezes, pois isso sacrifica bastante o desempenho do jogo. O correto é criar um objeto global que seja utilizado em toda a aplicação. O XNA, no entanto, oferece uma solução mais inteligente permitindo a reutilização do objeto que não é global: o game service

É possível pensar nele como um serviço disponível para qualquer coisa referente ao jogo. A ideia por trás disso é que o componente seja dependente de certos tipos, ou serviços, para que funcione. Se esse serviço não estiver disponível ele não funcionará bem. Neste caso, o método Draw procurará um SpriteBatch ativo diretamente na coleção GameServices.

Sabendo disso, adicione o seguinte código logo após a criação do SpriteBatch no método LoadContent da classe Game1:

//Adiciona o serviço SpriteBatch
Services.AddService(typeof(SpriteBatch), spriteBatch);

Todos os GameComponents definidos no seu jogo utilizarão esse SpriteBatch.

Explicado o GameService, agora é a vez da classe Ship que acabou de ser criada. O método Update verifica pelas entradas do teclado para atualizar o atributo Posicao e alterar a posição da nave na tela. Nele você ainda confere se a nave está dentro dos limites da tela/janela. Caso não esteja, o código a coloca dentro da área visível. 

O método GetBound apenas retorna o retângulo dos limites do sprite da nave. Este será utilizado nos próximos artigos para fazer testes de colisão com os asteroides.

Finalmente, o PutinStartPosition coloca a nave na posição inicial, horizontalmente centralizada na parte inferior da tela. Esse método será acionado quando você precisar deixar a nave em sua posição inicial; por exemplo, no começo de um novo round

Agora teste este GameComponent. Crie o método Start na classe Game1. Ele será utilizado para inicializar os objetos do game (apenas o jogador, por enquanto), como no código abaixo:

/// <summary>
/// Inicializa o round do jogo
/// </summary>
private void Start()
{
   //Cria (se necessário) e coloca o jogador na posição inicial
   if (player == null)
   {
      //Adiciona o componente do jogador
      player = new Ship(this, ref asteroidsTexture);
      Components.Add(player);
    }

    player.PutinStartPosition();
}

Não se esqueça de declarar o atributo player usado neste método na classe Game1:

//Objeto da nave do jogador
private Ship player;

Agora, de volta à lógica do jogo – geralmente desenvolvida no Update – para iniciar a rodada caso ela ainda não tenha começado. Adicione o seguinte código nesse método:

//Iniciar o jogador se não já estiver inicializado
if (player == null)
   Start();

Finalmente, resta apenas um detalhe a ser feito. O método Draw do seu jogo está desenhando somente o plano de fundo. Vamos fazê-lo traçar todos os GameComponent do projeto.

Adicione o seguinte código logo após o do desenho no plano de fundo.

//Inicia a renderização dos sprites
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
//Desenha os gamecomponents (sprites inclusos)
base.Draw(gameTime);
//Encerra a renderização de sprites
spriteBatch.End();

Agora sim, você pode salvar o seu projeto e executá-lo. Movimente a espaçonave na tela com as setas do teclado. Observe que toda a lógica da nave é manipulada pelo próprio componente que você criou, apesar do XNA acionar o método Update automaticamente através do base.Update da classe Game1.

Bacana, não? Vamos agora colocar os asteroides para aparecerem também! Não, não, brincadeira. O artigo de hoje foi bem extenso e contém informação suficiente para você pensar e estudar por uma semana ou mais!

Talvez você não tenha percebido, mas já está trabalhando no seu primeiro jogo em 2D: “Asteroides”! Fizemos o projeto, preparamos o plano de fundo e criamos o componente que representará a espaçonave controlada pelo jogador. O próximo artigo ensinará como criar o componente responsável pelos asteroides e construir a lógica do jogo como um todo. Fique esperto e não perca!

O código-fonte do projeto deste artigo pode ser obtido acessando o link: http://sdrv.ms/Zxykjf. Caso tenha dúvidas, entre em contato pelo e-mail sergioliveira@nintendoblast.com.br.

Revisão: Bruna Lima
Capa: Diego Migueis
Sergio Oliveira escreve para o Xbox 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 do mesmo.

Comentários

Google+
Disqus
Facebook