O XNA Game Studio foi um poderoso framework para desenvolvimento de jogos, tornando possível a criação de um projeto completo, onde todas as estruturas para tratamento de imagens, sons, controles, está acessível de forma bastante prática para o desenvolvedor. Aqui criaremos, em um artigo de duas partes, um jogo onde um helicóptero deve passar por entre arcos que voam pela tela, mostrando toda a estrutura para que outros jogos sejam desenvolvidos a partir de conceitos similares.

Microsoft XNA é uma framework que serve para o desenvolvimento de jogos para PCs com Windows, para o console Xbox 360 e para Windows Phone 7. Ele vem a ser um substituto ao Managed DirectX e pode ser baixado gratuitamente.

  • Linguagem: C#
  • Plataforma: Microsoft.NET, Xbox 360, Zune, Windows Phone 7
  • Estado do desenvolvimento: Descontinuado em 2013
  • Versão estável: 4.0 (16 de setembro de 2010)

Acredito que grande parte dos profissionais de TI foram atraídos para essa área através dos jogos para computadores e videogames que fizeram parte de sua infância e provavelmente o acompanham na vida adulta (é o caso deste que aqui escreve). Mas por não ser uma área muito movimentada no Brasil (o que vem mudando gradativamente), a especialização nesta área ainda não oferece trabalho a todos que teriam vontade de trabalhar nela.

Desde sempre, para se desenvolver um jogo, o conhecimento de programação, rotinas de acesso às interfaces gráficas, de áudio ou rede sempre exigiram bastante tempo e dedicação. O que mantinha a vontade de fazer e a real execução de uma idéia bem distante.

Pensando nesses desenvolvedores casuais, ou aspirantes no mundo dos jogos eletrônicos, foi desenvolvido o XNA Game Studio, já na versão 2.0. Com ele, o criador pode desenvolver com maior facilidade suas idéias, já que o poderoso framework é quem se preocupará com a interface com os dispositivos gráficos, áudio ou rede. Contando com uma dose de criatividade, conhecimento de C# e força de vontade, é possível criar seus jogos e divertir-se no Windows ou Xbox 360. Detalhe: todas as ferramentas estão disponíveis gratuitamente.

Instalando e executando o XNA Game Studio

A primeira coisa a fazer é baixar o Visual C# 2005 Express Edition e o XNA Game Studio. Instale os dois softwares e siga as instruções na tela, sendo necessário instalar o Visual C# Express em primeiro lugar. Caso você já tenha o Visual Studio 2005 instalado, não é necessário instalar o Visual C# Express.

Verifique que no menu Iniciar > Todos os Programas , foi criado o atalho para o “Microsoft Visual Studio 2005”, dentro de “Microsoft XNA Game Studio 2.0”. Clique em File > New > Project e repare que algumas novas opções estão disponíveis (Figura 1). Selecione Windows Game (2.0) e dê o nome à solução de “MyCopter” (este é o nome do jogo, logo à frente explicaremos melhor).

Criação de projeto de jogos
Figura 1.Criação de projeto de jogos.

Repare na estrutura básica de arquivos que é criada, com destaque ao arquivo Game1.cs, que é onde, basicamente, tudo acontece. Normalmente alguns desenvolvedores apagam este arquivo e o reescrevem novamente, mas vamos aproveitar aqui o que já está pronto (Figura 2). Execute o projeto pressionando F5 e você verá somente uma tela azul.

Estrutura básica de um projeto Windows Game (2.0)
Figura 2.Estrutura básica de um projeto Windows Game (2.0), destacando a classe Game1.

O Jogo e seus conceitos

Neste artigo iremos desenvolver um jogo simples, onde um helicóptero irá sobrevoar uma área sem fim com diversos arcos (veja uma tela do jogo na Figura 3), tendo como objetivo passar pelo centro do maior número possível de arcos sem colidir com eles (muitos já devem ter visto alguma versão em sites de jogos casuais, jogados no próprio browser). É um projeto simples, mas que envolve uma quantidade enorme de conceitos e aprendizagens e com isso o desenvolvedor terá contato com quase tudo que é necessário para programar o seu próprio jogo, bastará ter uma boa idéia.

Na parte 1 você será apresentado ao framework, chegando a exibir todas as imagens do jogo e movimentar as nuvens. Na parte 2, veremos como movimentar o helicóptero e os arcos, inserindo os métodos de verificação de colisão entre objetos.

Imagem do jogo que iremos desenvolver, contendo o helicóptero, arcos e as nuvens, além do contador de vidas e pontos
Figura 3. Imagem do jogo que iremos desenvolver, contendo o helicóptero, arcos e as nuvens, além do contador de vidas e pontos.

Antes de começarmos realmente, vamos estabelecer alguns conceitos comuns quando se trata de jogos para computador. Primeiro a Figura 4.

Fluxo comum de jogos
Figura 4. Fluxo comum de jogos.

Nela você pode acompanhar o fluxo normal de qualquer jogo, onde, na ordem, devemos primeiramente iniciar os componentes gráficos, que gerenciam o que será enviado para a tela, os de entrada de dados, normalmente teclado, mouse ou joystick e os gerenciadores de sons.

Logo em seguida os recursos que serão gerenciados pelos controladores são carregados na memória, como imagens, modelos 3D, fontes, sons, músicas etc.

O próximo passo é onde tudo é “misturado” para formar o jogo, nele ocorre o gerenciamento do que o jogador está enviando de entrada e a partir disso gerar o resultado, por exemplo, mover um personagem, verificar se houve alguma colisão, disparar o som equivalente à ação, continuar a tocar a música de fundo.

Toda essa verificação de entradas e status é repetida infinitamente, até que alguma ação de sair do jogo, incluindo derrota ou vontade do usuário, seja disparada.

Traduzindo para o XNA Game Studio 2.0, os métodos da classe Game1.cs (que já deve estar incluída no seu projeto), são:

  • Game1(): Construtor da classe, onde os gerenciadores de gráficos são inicializados;
  • Initialize(): Inicialização de demais componentes, não gráficos;
  • LoadContent(): Aqui é onde são carregadas as figuras, os sons, os arquivos de fontes, modelos 3D etc.;
  • Run(): Início da execução do jogo, fazendo com que os métodos seguintes sejam executados em looping:
  • Update(): Nesse método são incluídos todos os cálculos de posições de objetos, captura de comandos, execução de música e sons;
  • Draw(): Após os cálculos do método Update(), a imagem precisa ser formada para exibir ao jogador. O método Draw será o responsável;
  • UnloadContent(): Ao receber um comando para parar de executar os métodos Update e Draw, o jogo será terminado e a memória precisa ser liberada através deste método.

Cada método será apresentado conforme necessário. Acompanhe na Figura 5 o diagrama com as principais classes do nosso jogo. Utilizaremos a classe GameObject para centralizar algumas propriedades e métodos comuns para qualquer item que será exibido, utilizando herança para os itens específicos, como as classes Copter e EnvironmentItem.

Diagrama de classes do projeto
Figura 5. Diagrama de classes do projeto.

Todo o conceito na criação games exige um estudo maior do que está contido neste artigo, que é direcionado para uma abordagem prática. Para maior especialização sobre conceitos, design de games, alguns livros estão disponíveis e indicarei alguns ao fim do artigo.

Definindo os objetos do jogo

Em um jogo 2D, que é o que estamos desenvolvendo, os objetos são representados na tela através do que chamamos de Sprites. Portanto o helicóptero será um Sprite, assim como as nuvens e os arcos. As classes que definiremos a seguir irão conter as propriedades de cada Sprite, como posição, velocidade, aceleração etc.

A primeira classe a ser definida é a GameObject, contendo algumas propriedades e somente o método de checagem de colisão com outro item de jogo. Nela incluo várias propriedades comuns aos diversos objetos do jogo (controle do Sprite) e outras que comentaremos mais para frente, como SourceRectangle e DestinationRectangle.

Crie uma pasta Objects na raiz do projeto e dentro dela o arquivo GameObject.cs, que deverá conter a Listagem 1. Repare nas referências a alguns namespaces do XNA. Eles serão úteis quando utilizarmos objetos do tipo Vector2 ou Rectangle, nativos do framework. O primeiro pode ser usado para várias propriedades que exigem dois valores que, nesse caso, serão do tipo float. Os Rectangles serão usados para definir as áreas de colisão.

Listagem 1. Classe GameObject.cs.

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;



namespace MyCopter.Objects

{

 public class GameObject

 {



  #
  region "Variáveis e Propriedades"



  Vector2 _velocity;

  Vector2 _position;

  List _colisionarea;

  Texture2D _image;

  Rectangle _sourceRectangle;

  Rectangle _destinationRectangle;

  bool _isVisible;

  bool _scoreUpdate;



  //Propriedade que indica se a pontuação deve ser atualizada

  public bool ScoreUpdate

  {

   get {
    return _scoreUpdate;
   }

   set {
    _scoreUpdate = value;
   }

  }



  //Propriedade que indica se o objeto é visível no jogo

  public bool IsVisible

  {

   get {
    return _isVisible;
   }

   set {
    _isVisible = value;
   }

  }



  public Rectangle SourceRectangle

  {

   get {
    return _sourceRectangle;
   }

   set {
    _sourceRectangle = value;
   }

  }



  public Rectangle DestinationRectangle

  {

   get {
    return _destinationRectangle;
   }

   set {
    _destinationRectangle = value;
   }

  }



  public Texture2D Image

  {

   get {
    return _image;
   }

   set {
    _image = value;
   }

  }



  public Vector2 Velocity

  {

   get {
    return _velocity;
   }

   set {
    _velocity = value;
   }

  }



  public Vector2 Position

  {

   get {
    return _position;
   }

   set

   {

    _position = value;

   }

  }



  public List ColisionArea

  {

   get {
    return _colisionarea;
   }

   set {
    _colisionarea = value;
   }

  }



  #
  endregion



  public GameObject()

  {

   ColisionArea = new List();

  }



  public bool CheckColision(Rectangle testcolision)

  {

   bool RetVal = false;

   for (int i = 0; i < _colisionarea.Count; i++)

   {

    if (this._colisionarea[i].Intersects(testcolision))

    {

     RetVal = true;

     break;

    } else

    {

     RetVal = false;

    }

   }

   return RetVal;

  }

 }

}

O único método que incluiremos nessa classe é o de verificação de colisão entre os objetos. No XNA ou em qualquer ambiente para desenvolvimento de jogos, o sistema de colisão pode ser por áreas de colisão, por área visível do sprite (pixel) etc. Neste caso optei por colocar um retângulo de colisão tendo o mesmo tamanho da imagem exibida e, para os arcos, colocaremos um retângulo (tipo Rectangle) na parte superior e outro na inferior, no mesmo esquema da Figura 6.

Retângulos de colisão dos objetos
Figura 6. Retângulos de colisão dos objetos

O sistema de colisão consiste em comparar os retângulos dos objetos e verificar se existem pontos em comum entre eles, através do método Intersects do objeto Rectangle.

Com a classe GameObject pronta, podemos passar para as outras que serão classes filha dessa. A primeira a ser apresentada será a EnvironmentItem, que será utilizada para dois objetos no jogo, as nuvens e os arcos (adicione o arquivo EnvironmentItem.cs dentro da pasta Objects). Na verdade somente mudei o método construtor, para incluir dois parâmetros, de posição e velocidade iniciais, mas essa classe poderá receber várias outras mudanças conforme o jogo for evoluindo (Listagem 2).

Listagem 2. Classe EnvironmentItem, que representará as nuvens e arcos do projeto.

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework;

 

namespace MyCopter.Objects

{

    class EnvironmentItem:GameObject

    {

 

        public EnvironmentItem(Vector2 InitialPosition,

                        Vector2 InitialVelocity)

        {

            this.Position = InitialPosition;

            this.Velocity = InitialVelocity;

        }

 

    }

}

A terceira classe que utilizaremos no jogo é a que trata do objeto helicóptero em si, onde apenas acrescento propriedades para controle da aceleração do mesmo. Acompanhe na Listagem 3 o conteúdo da classe, que também deve ser criada dentro da pasta Objects como nome Copter.cs. Repare que no construtor deveremos receber a posição e velocidades iniciais.

Listagem 3. Conteúdo da classe Copter.

using System;

using System.Collections.Generic;

using System.Text;

using Microsoft.Xna.Framework;

 

namespace MyCopter.Objects

{

    public class Copter:MyCopter.Objects.GameObject

    {

        private float _aceleration;

        private bool _isAccelerating;

        private float _maxVelocity;

       

        public float MaxVelocity

        {

            get { return _maxVelocity; }

            set { _maxVelocity = value; }

        }

 

        public bool IsAccelerating

        {

            get { return _isAccelerating; }

            set { _isAccelerating = value; }

        }

       

        public float Aceleration

        {

            get { return _aceleration; }

            set {_aceleration = value;}

        }

 

        public Copter(Vector2 InitialPosition,

                        Vector2 InitialVelocity)

        {

            this.Position = InitialPosition;

            this.Velocity = InitialVelocity;

            this.Aceleration = 0;

        }

    }

}

Com as classes definidas, o próximo passo é incluir no projeto as imagens que serão utilizadas para representar os objetos. Normalmente, nos jogos, são utilizados mapas de imagens contendo cada quadro de animações, partes dos cenários etc. No nosso caso utilizaremos quatro imagens, uma contendo a animação do helicóptero, outro contendo os tipos de nuvens e mais duas que formarão o desenho do arco. Para melhor organizar esses arquivos, criarei uma pasta chamada Images dentro do sub-projeto Content, onde adicionarei as imagens. Clique com o botão direito sobre esta pasta, Add > Existing Item e localize onde as imagens estão salvas (Figura 7).

Adição das imagens que serão usadas no jogo
Figura 7. Adição das imagens que serão usadas no jogo.

Na Figura 8 estão as imagens todas juntas, a área retangular à volta delas representa o arquivo no seu tamanho correto e como será trabalhado para formar as animações e imagens estáticas. O nome do arquivo está logo abaixo.

Estamos acostumados a pensar em eixos X e Y de uma forma, mas quando se trata de jogos em XNA, o eixo Y é invertido, sendo sempre o ponto 0 em ambas as posições no canto superior esquerdo das imagens, aumentando em X para a direita e em Y para baixo

Imagens do Jogo
Figura 8. Imagens do Jogo.

O projeto de propriedades Content Pipeline, é uma parte muito importante do projeto, pois é como o XNA trabalha com os diversos tipos de arquivos relacionados a imagens, modelos 3D, sons e fontes. Quando o projeto é compilado, cada arquivo é convertido para um formato próprio do framework permitindo um acesso rápido e eficaz a esse conteúdo, que em um projeto feito em outro tipo de plataforma, obrigaria a equipe de desenvolvimento a ter muito mais cuidado ao preparar tais arquivos de conteúdo.

As imagens deste jogo foram criadas pelo autor e estarão disponíveis para download no site da revista .Net Magazine, assim como todo o projeto aqui apresentado.

Exibindo os objetos

O primeiro passo para exibir imagens no jogo é carregá-las na memória durante a inicialização. Veja novamente o segundo quadro da Figura 4, é lá que estamos agora. Vamos incluir o código para carregamento das imagens dentro do método LoadContent da classe Game1.cs (Listagem 4).

Algumas variáveis também precisam ser incluídas no corpo da classe Game1 (também estão na Listagem 4), elas é que irão armazenar as figuras carregadas, através do tipo Texture2D, próprio do framework XNA.

Listagem 4. Método LoadContent alterado e variáveis da classe Game1.

Texture2D _imgCopter;

Texture2D _imgCloud;

Texture2D _imgArcBack;

Texture2D _imgArcFront;

 

protected override void LoadContent()

{

    spriteBatch = new SpriteBatch(GraphicsDevice);

 

    _imgCopter = Content.Load("Images\\Copter_SpriteTile");

    _imgCloud = Content.Load("Images\\Clouds_SpriteTile");

    _imgArcBack = Content.Load("Images\\Arc_Sprite_Back");

    _imgArcFront = Content.Load("Images\\Arc_Sprite_Front");

}

Para o código ficar mais organizado e legível, eu vou criar um método de definição para cada objeto, onde cada um será instanciado e com as propriedades principais definidas. Primeiro vamos incluir mais alguns objetos na classe Game1 (Listagem 5).

Para as nuvens e arcos vamos criar uma lista destes objetos, já que eles serão repetidos na tela, então fica mais fácil manipular um objeto deste tipo, do que trabalhar com vários objetos do mesmo tipo, onde as propriedades são semelhantes.

Listagem 5. Objetos da classe Game1, incluindo variáveis de inicialização.

Objects.Copter _copter;

List _cloud;

List _arc;

Nos métodos de definição das nuvens e arcos, vamos verificar se as listas já foram instanciadas, verificando se são diferentes de null. Caso já exista, a lista será reiniciada. Esses métodos devem ser chamados dentro do método Initialize, da classe Game1(Listagem 6).

Para exemplificar a exibição de um arco completo, utilizaremos as propriedades do objeto GraphicsDeviceManager, nomeado graphics na classe Game1, e já inserido por padrão quando criamos o projeto. Com ele temos as propriedades do dispositivo gráfico, aqui utilizaremos as dimensões da tela como referência para posicionar os objetos. É muito importante ter essa referência, principalmente quando o jogo pode ser redimensionado para o tamanho da tela do jogador, se usarmos valores fixos, os objetos podem aparecer em diferentes posições para cada ambiente, o que com certeza irá causar problemas.

Junto com essa propriedade do objeto graphics, coloquei um valor randômico, para realizar o “sorteio” de onde o arco irá aparecer.

Como cada arco é formado por duas imagens, uma de fundo e outra na frente, para cada arco, vamos inserir dois objetos na lista, manipulando eles sempre aos pares.

Listagem 6. Definição da lista de nuvens, arcos e método Initialize atualizado.

protected override void Initialize()

{

// TODO: Add your initialization logic here

 

base.Initialize();

DefineArcs();

DefineClouds();

}

private void DefineClouds()

{

    if (_cloud == null)

    {

        _cloud = new List();

    }

    else

    {

        _cloud.Clear();

    }

}

 

private void DefineArcs()

{

    Random rand = new Random();

    if (_arc == null)

    {

        _arc = new List();

        Objects.EnvironmentItem _newArcBack =

            new MyCopter.Objects.EnvironmentItem(new Vector2(rand.Next(graphics.PreferredBackBufferWidth)

                            , rand.Next(graphics.PreferredBackBufferHeight - _imgArcBack.Height)), new Vector2(0, 0));

        Objects.EnvironmentItem _newArcFront =

                    new MyCopter.Objects.EnvironmentItem(_newArcBack.Position, _newArcBack.Velocity);

        _arc.Add(_newArcFront);

        _arc.Add(_newArcBack);

    }

    else

    {

        _arc.Clear();

    }

}

Nesse momento, se você executar o projeto, nada aparecerá. Isso porque ainda não estamos enviando as informações para a tela. Faremos isso através do objeto spriteBatch, instância da classe de mesmo nome, que junto com o objeto graphics, também é criado automaticamente no projeto, dentro da classe Game1.

Este objeto é responsável por desenhar todo tipo de imagem e texto na tela, através dos métodos Draw e DrawString, contando com diversos overloads para cada um, oferecendo várias possibilidades, como rotação de sprites, efeitos, redimensionamento etc.

Para começar a desenhar os objetos devemos criar um bloco, dentro do método Draw da classe Game1, chamando o método Begin e o End do objeto spriteBatch, veja na Listagem 7 como ficou.

Listagem 7. Desenhando os objetos na tela.

protected override void Draw(GameTime gameTime)

{

    graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

 

    spriteBatch.Begin();

    foreach (Objects.EnvironmentItem cloud in _cloud)

    {

        spriteBatch.Draw(_imgCloud, cloud.DestinationRectangle, cloud.SourceRectangle, Color.White);

    }

 

    for (int i = 0; i < _arc.Count - 1; i = i + 2)

    {

        spriteBatch.Draw(_imgArcBack, _arc[i].Position, Color.White);

    }

   

    for (int i = 0; i < _arc.Count - 1; i = i + 2)

    {

        spriteBatch.Draw(_imgArcFront, _arc[i + 1].Position, Color.White);

    }

    spriteBatch.End();

    base.Draw(gameTime);

}

Execute o projeto algumas vezes e repare como em cada uma, o arco aparecerá em uma posição, tudo por causa do objeto de randomização que utilizamos na definição do arco. Não estranhe se nenhuma nuvem for desenhada, temos trabalho a fazer ainda para fazê-las aparecer.

Exibindo e animando as nuvens

Você deve ter reparado que os três tipos de nuvens estão todos no mesmo arquivo com extensão png, então teremos que administrar essa imagem, exibindo um tipo de nuvem para cada objeto da lista que os contém. Para definir o que exibir da imagem, vamos utilizar as propriedades do tipo Rectangle: SourceRectangle e DestinationRectagle. Sendo a primeira referente à área que será desenhada da imagem total, e na segunda é definida a posição da imagem em relação à tela.

Vamos criar então o método ManageClouds somente para a exibição das nuvens (Listagem 8). Novamente usaremos o objeto random, desta vez para três funções, para sortear a posição no eixo X e Y, para sortear qual modelo de nuvem será exibido e qual a velocidade no eixo X que a nuvem terá.

A criação da lista será feita no bloco IF, e, quando chegarmos à quantidade máxima de nuvens estabelecidas, doze nesse caso, partiremos para a movimentação no bloco ELSE.

Listagem 8. Método ManageClouds.

private void ManageClouds()

{

 Random rand = new Random();

 if ((_cloud == null) || (_cloud.Count < 12))

 {

  //criação das nuvens, até que alcancemos 12 nuvens na lista

  Objects.EnvironmentItem _newCloud =

   new MyCopter.Objects.EnvironmentItem(new Vector2(rand.Next(graphics.PreferredBackBufferWidth)

    , rand.Next(graphics.PreferredBackBufferHeight - _imgCloud.Height)), new Vector2((float) - 1 f * rand.Next(25, 40) / 100, 0));

  //criação do sourceRectangle, sorteando qual modelo de nuvem irá aparecer

  _newCloud.SourceRectangle = new Rectangle(rand.Next(3) * _imgCloud.Width / 3,

   0, _imgCloud.Width / 3,

   _imgCloud.Height);

  _cloud.Add(_newCloud);

 } else if (_cloud != null)

 {

  //Loop para atualização de cada item da lista de nuvens

  foreach(Objects.EnvironmentItem cloud in _cloud)

  {

   //se a nuvem passa dos limites no eixo X, será reposicionada

   if (cloud.Position.X <= 0 - (_imgCloud.Width / 3))

   {

    cloud.Position = new Vector2(graphics.PreferredBackBufferWidth + rand.Next(graphics.PreferredBackBufferWidth)

     , rand.Next(graphics.PreferredBackBufferHeight - _imgCloud.Height));

   }

   //se não atingiu os limites, acrescentar à posição o valor da velocidade
   else

   {

    cloud.Position = new Vector2(cloud.Position.X + cloud.Velocity.X, cloud.Position.Y);

   }

   //definição do destinationRectangle, com a posição da nuvem em relação à tela

   cloud.DestinationRectangle = new Rectangle((int) cloud.Position.X,

    (int) cloud.Position.Y,

    _imgCloud.Width / 3,

    _imgCloud.Height);

  }

 }

}

A velocidade de movimentação da nuvem deve variar entre 0,25 e 0,40 pixels por tempo de update, por isso a fórmula rand.Next(25, 40) / 100 é utilizada, sendo o valor multiplicado por -1, já que as nuvens se movimentarão da direita para esquerda.

Nota: A medida de espaço de um objeto na tela é feita em pontos (pixels) da tela, e o tempo pode ser calculado pelo tempo que é gasto para se atualizar a tela. Por isso que medimos a velocidade através de quantos pontos o objeto se movimenta por atualização da tela.

Veja que quando se instancia o SourceRectangle, estamos multiplicando um valor randômico pela largura da imagem dividida por três, que é o tamanho da imagem que será exibida. Portanto se for sorteado, por exemplo, o valor 1, teremos a segunda imagem exibida (Figura 9).

Fórmula para sorteio da nuvem que será exibida
Figura 9. Fórmula para sorteio da nuvem que será exibida.

Durante a movimentação do sprite da nuvem, devemos também verificar se ela passou dos limites da tela, e caso isso ocorra, vamos colocá-la novamente partindo do lado direito da tela, sorteando novamente a posição em Y que ela ficará.

Inclua a chamada ao método ManageClouds, dentro do método Update, já presente à classe Game1 (Listagem 9). Este método deve concentrar todas as atualizações de objetos, já que ele é sempre executado antes da exibição dos objetos na tela (método Draw).

Listagem 9. Método Update, com a chamada ao ManageClouds.

protected override void Update(GameTime gameTime)

{

    // Allows the game to exit

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

        this.Exit();

 

    // TODO: Add your update logic here

    ManageClouds();

    base.Update(gameTime);

}

Execute o projeto e veja as nuvens se movimentando da direita para a esquerda, infinitamente.

Nesta primeira parte ficamos conhecendo alguns conceitos para o desenvolvimento de jogos, assim como a exibição de imagens (sprites). Com o conhecimento sobre a exibição de arquivos com várias imagens, será mais simples na próxima etapa, quando utilizaremos conceitos parecidos para animar nosso helicóptero.

O próximo passo será para fazer a animação dos arcos, que consiste em definir a quantidade máxima e fazê-los passar pela tela da direita para a esquerda infinitamente, dando ao jogador a impressão de que o helicóptero está viajando em uma grande distância.

Com os arcos prontos, vamos “ligar os motores” do helicóptero e controlar a interação deles com os arcos, através dos métodos de colisão. Sempre controlando o placar e a quantidade de vidas, que irão permitir o jogador continuarem jogando mesmo colidindo com alguns arcos.

Considera-se que o desenvolvedor que for acompanhar esta segunda parte do artigo, tenha lido e acompanhado a primeira parte, e tenha o projeto criado em seu micro, para poder dar continuidade ao jogo.

Animando os Arcos

Finalizamos a primeira parte do artigo concluindo a animação das nuvens, que somente irão fazer o efeito de profundidade, como plano de fundo para a ação em si, que será entre os arcos e o helicóptero.

A animação dos arcos é muito parecida com o controle feito para as nuvens, já que ambos os objetos devem percorrer a tela e recomeçar a trajetória em outra posição quando passarem dos limites da janela.

O primeiro passo vai ser modificar o método de definição dos arcos que criamos na primeira parte do artigo (Listagem 1). Este método fazia a colocação de um arco em um lugar aleatório da tela. E para movimentar os arcos teremos um método próprio (ManageArcs, assim como o ManageClouds, para as nuvens).

O tipo de objeto que irá popular a lista é do tipo EnvironmentItem, criado na primeira parte do artigo, e já utilizado para as nuvens.

Listagem 1. Criação da lista de arcos.

private void DefineArcs()

{

    if (_arc == null)

    {

        _arc = new List<MyCopter.Objects.EnvironmentItem>();

    }

    else

    {

        _arc.Clear();

    }

}

A animação dos arcos, como já foi dito, é muito semelhante à das nuvens. A diferença é que todos os arcos terão a mesma velocidade, e temos que sempre fazer as duas partes que compõem o arco (parte de trás e frente) se movimentarem na mesma velocidade, passando a impressão de que é uma peça só.

No próximo trecho de código, apresentado na Listagem 2, é visto o método de gerenciamento da movimentação dos arcos. Além da criação das instâncias dentro da lista de arcos, repare que também já são definidas as áreas de colisão (verifique a Figura 6 da primeira parte do artigo, onde é exibida a posição aproximada das áreas de colisão dos arcos).

Listagem 2. Método ManageArcs().

private void ManageArcs()

{

 //Declareção do objeto Random, para "sorteio" das posições dos arcos a cada retorno para a tela

 Random rand = new Random();

 /*

  * o próximo passo é instanciar as 3 instâncias do arco, mas por ser dividido em 2 partes,

  * devemos acrescentar 2 objetos na lista para cada um.

  */

 if (_arc.Count < 5)

 {

  Objects.EnvironmentItem _newArcBack;

  /*

   * Caso seja o primeiro objeto, vamos definir sua posição logo após o final da tela,

   * e os próximos, partindo dele, dividindo sempre em partes iguais.

  */

  if (_arc.Count == 0)

  {

   _newArcBack =

    new MyCopter.Objects.EnvironmentItem(new Vector2(rand.Next(graphics.PreferredBackBufferWidth)

     , rand.Next(graphics.PreferredBackBufferHeight - _imgArcBack.Height)), new Vector2(-3 f, 0));

  } else

  {

   _newArcBack =

    new MyCopter.Objects.EnvironmentItem(new Vector2(_arc[_arc.Count - 1].Position.X + graphics.PreferredBackBufferWidth / 2

     , rand.Next(graphics.PreferredBackBufferHeight - _imgArcBack.Height)), _arc[_arc.Count - 1].Velocity);

  }

  /*

   * Sempre ao instanciar uma parte do fundo do arco,

   * temos que instanciar a frente, acompanhando o caminho da outra parte.

   */

  Objects.EnvironmentItem _newArcFront =

   new MyCopter.Objects.EnvironmentItem(_newArcBack.Position, _newArcBack.Velocity);

  /*

   * Além das partes da figura, as áreas de colisão também devem ser criadas e acompanhar cada

   * parte do arco.

   */

  _newArcFront.ColisionArea.Add(new Rectangle((int) _newArcFront.Position.X + (int)(_imgArcFront.Width / 5),

   (int) _newArcFront.Position.Y,

   _imgArcFront.Width / 5,

   _imgArcFront.Height / 10));

  _newArcFront.ColisionArea.Add(new Rectangle((int) _newArcFront.Position.X + (int)(_imgArcFront.Width / 5),

   (int) _newArcFront.Position.Y + (int) 9 * (_imgArcFront.Height / 10),

   _imgArcFront.Width / 5,

   _imgArcFront.Height / 10));

  //Sempre que os arcos retornam para o ínício, deve estar visível (utilizado pelo sistema de colisão)

  _newArcFront.IsVisible = true;

  //Variável utilizada pelo sistema de contagem de pontos

  _newArcFront.ScoreUpdate = false;

  // Após criadas as instâncias, elas devem ser adicionadas à lista de arcos.

  _arc.Add(_newArcBack);

  _arc.Add(_newArcFront);

 } else if (_arc != null)

 {

  /*

   * No pedaço de código a seguir, é feito o posicionamento do arco à cada update da tela.

   * Se o arco sai da tela, ele é reposicionado novamente após o final da tela,

   * sendo a altura definida pelo objeto Random.

   */

  Vector2 nextposition = _arc[_arc.Count - 1].Position;

  for (int i = 0; i < _arc.Count - 1; i = i + 2)

  {

   if (_arc[i].Position.X <= 0 - (_imgArcBack.Width))

   {

    _arc[i].Position = new Vector2(nextposition.X + (graphics.PreferredBackBufferWidth / 2),

     rand.Next(graphics.PreferredBackBufferHeight - _imgArcBack.Height));

    _arc[i + 1].Position = _arc[i].Position;

    //Os arcos quando voltam para a tela, precisam estar visíveis, e não marcar ponto

    _arc[i + 1].IsVisible = true;

    _arc[i + 1].ScoreUpdate = false;

   } else

   {

    _arc[i].Position = new Vector2(_arc[i].Position.X + _arc[i].Velocity.X, _arc[i].Position.Y);

    _arc[i + 1].Position = _arc[i].Position;

   }

   _arc[i + 1].ColisionArea[0] = new Rectangle((int) _arc[i + 1].Position.X + (int)(_imgArcFront.Width / 3),

    (int) _arc[i + 1].Position.Y,

    _imgArcFront.Width / 3,

    _imgArcFront.Height / 8);

   _arc[i + 1].ColisionArea[1] = new Rectangle((int) _arc[i + 1].Position.X + (int)(_imgArcFront.Width / 3),

    (int) _arc[i + 1].Position.Y + (int) 7 * (_imgArcFront.Height / 8),

    _imgArcFront.Width / 3,

    _imgArcFront.Height / 8);

   nextposition = _arc[i].Position;

  }

 }

}

Se você executar o projeto agora, os arcos terão sumido. Isso porque ainda não inserimos a chamada ao método de gerenciamento de arcos, dentro do método Update da classe Game1. Logo abaixo da chamada ao método ManageClouds, coloque a do método ManageArcs. Em seguida execute o projeto e veja o resultado, com as nuvens ao fundo, e os arcos passando à frente.

O próximo passo é colocar o helicóptero em cena.

Ligando e exibindo o Helicóptero

Para exibirmos o helicóptero, primeiro precisamos entender como é o processo de animação dos quadros, que no caso são 3, dando a impressão ao jogador de que as hélices estão rodando em alta velocidade. A Figura 1 mostra como é a imagem que contém os quadros da animação do helicóptero. Essa imagem já deve ter sido adicionada na primeira parte do artigo.

Utilizaremos novamente o SourceRectangle e DestinationRectangle, justamente como as nuvens. Mas no primeiro caso, apenas sorteávamos qual das nuvens deveriam ser exibidas. Agora, a cada atualização de tela, o SourceRectangle será mudado de posição, para acomodar o quadro que deverá ser exibido no momento, resultando na animação do helicóptero.

Helicóptero, arquivo Copter_SpriteTile.png
Figura 1. Helicóptero, arquivo Copter_SpriteTile.png.

Mantendo o padrão, iremos criar dois métodos principais para a manipulação do helicóptero, um de definição (DefineCopter) e outro de manipulação (ManageCopter). Na Listagem 3, estão algumas variáveis e constantes que iremos utilizar, adicione seu conteúdo no início da classe Game1.

Listagem 3. Variáveis e constantes para o helicóptero.

//Número de frames da animação do helicóptero

int _copterFrames = 3;

//Quadro que atualmente está sendo exibido na tela, deve ser alterado a cada update da tela

int _currentCopterFrame;

//Posição inicial do helicóptero, usada ao reiniciar o jogo

Vector2 _CopterInitialPosition;

//Variável que irá servir de controle do tempo

float _timer = 0f;

//Constante para controle do tempo para atualização dos quadros

float _interval = 40f;

Na Listagem 3 encontramos alguns objetos para controle de tempo. Eles serão necessários para controlar a taxa de atualização dos quadros do helicóptero. Se não houver esse controle, podemos encontrar diferença na exibição do helicóptero em diferentes computadores, nesse caso a taxa ficará fixa. A Listagem 4 contém o método responsável por esse controle.

Vamos chamar o método UpdateFrame dentro de Update, utilizando o valor da propriedade ElapsedGameTime em milissegundos, convertido para float, como parâmetro. Coloque o código a seguir no fim do método Update, logo antes de base.Update:


UpdateFrame((float)gameTime.ElapsedGameTime.TotalMilliseconds);
Listagem 4. Método UpdateFrame, para controle da taxa de atualização.

//O parâmetro do método é a quantidade em milisegundos do tempo gasto entre as atualizações da tela

private void UpdateFrame(float elapsedTime)

{

    //O tempo gasto é acrescentado à variável de controle de atualização

    _timer += elapsedTime;

    //Se a variável alcança o limite da taxa de atualização, o quadro será atualizado

    if (_timer > _interval)

    {

        //incremento da variável que controla o quadro a ser exibido

        _currentCopterFrame++;

        //retorno ao primeiro quadro, se o último estava sendo exibido

        if (_currentCopterFrame > _copterFrames - 1)

        {

            _currentCopterFrame = 0;

        }

        //Ao mudar de quadro, a variável de controle é zerada

        _timer = 0;

    }

    //definida a nova posição do SourceRectangle, exibindo o próximo quadro

    _copter.SourceRectangle = new Rectangle(_currentCopterFrame * _imgCopter.Width / 3, 0, _imgCopter.Width / 3, _imgCopter.Height);

}

Vamos ao método de definição (Listagem 5), onde todos os parâmetros iniciais para o gerenciamento do helicóptero ficam prontos para utilização. Cada linha importante de código está comentada, valendo destacar as definições do DestinationRectangle e da área de colisão do helicóptero, que acabam sendo iguais e acompanham sempre a posição e velocidade da aeronave. A chamada para este método deve estar no método Initialize, abaixo dos métodos DefineClouds e DefineArcs.

Listagem 5. Método DefineCopter.

private void DefineCopter()

{

 //Criação da instância da classe Copter

 if (_copter == null)

 {

  //Definição da posição inicial, baseado numa tela de 800x600 (tamanho padrão do XNA, podendo ser alterado)

  _CopterInitialPosition = new Vector2(20, 580);

  //Criação do objeto, definindo a posição e velocidade (zero), como parâmetros da classe

  _copter = new MyCopter.Objects.Copter(new Vector2(_CopterInitialPosition.X,

   _CopterInitialPosition.Y - _imgCopter.Height), new Vector2(0, 0));

  //Associação da imagem do helicóptero ao objeto

  _copter.Image = _imgCopter;

  //Máxima velocidade de subida do helicóptero. É negativa devido aos eixos do XNA

  _copter.MaxVelocity = -7 f;

 } else

 {

  //Caso o objeto já exista, a posição é definida como a inicial

  _copter.Position = _CopterInitialPosition;

 }

 //Definição do primeiro quadro a ser exibido

 _currentCopterFrame = 0;

 //Criação do DestinatioRectangle, que acompanha a posição do helicóptero

 _copter.DestinationRectangle = new Rectangle((int) _copter.Position.X,

  (int) _copter.Position.Y,

  _copter.Image.Width / 3,

  _copter.Image.Height);

 //Criação da área de colisão, que no nosso exemplo, acompanha o tamanho do DestinationRectangle

 _copter.ColisionArea.Add(new Rectangle(_copter.DestinationRectangle.X,

  _copter.DestinationRectangle.Y,

  _copter.DestinationRectangle.Width,

  _copter.DestinationRectangle.Height));

}

O último passo, e mais importante, é desenhar na tela o helicóptero. Para isso, no método Draw utilizaremos o objeto spriteBatch, que já está definido para desenhar as nuvens e as duas partes do arco.

Insira o código da Listagem 6 entre as chamadas para o desenho da parte de trás do arco e a parte da frente. Isso porque o XNA irá obedecer, nesse caso, a ordem em que os objetos estão sendo desenhados para exibir na tela, portanto se desejamos que o helicóptero passe entre os arcos, devemos utilizar essa técnica. Repare que um overload diferente do método Draw foi utilizado, mas nesse caso, o efeito é o mesmo do overload das nuvens, utilizei os valores padrão, apenas para exibir o overload. Procure sempre explorar os possíveis overloads, é uma dica para aprender outras possibilidades para utilização de uma classe.

Agora sim, ao executar o jogo, o helicóptero deverá aparecer na parte inferior esquerda, com a hélice “girando” (Figura 2).

Listagem 6. Desenhando o helicóptero.

spriteBatch.Draw(_copter.Image, _copter.DestinationRectangle, _copter.SourceRectangle,

 Color.White, 0, new Vector2(0, 0),

 SpriteEffects.None, 0);
Imagem atual do jogo, com a presença do helicóptero
Figura 2. Imagem atual do jogo, com a presença do helicóptero.

Movendo o helicóptero

Com todos os elementos principais do nosso jogo sendo exibidos na tela, vamos agora partir para a movimentação do personagem principal, que será feita através do teclado.

Nesse nosso exemplo, o helicóptero somente irá precisar de uma tecla de controle: aceleração, sendo utilizada a tecla para cima da área de setas do teclado. Se a tecla estiver sendo pressionada, haverá aceleração do helicóptero, se não, entra em ação a gravidade, fazendo-o descer.

Na Listagem 7, o método de controle do teclado é exibido, onde verificamos o objeto KeyboardState, nativo do XNA, que cuidará para que tenhamos em mãos as teclas pressionadas durante a atualização do jogo. Este método deve ser chamado dentro de Update, logo após o método de controle de quadros.

Listagem 7. Método ControlsManagement.

private void ControlsManagement()

{

    //Objeto responsável por verificar o estado do teclado

    KeyboardState currentState = Keyboard.GetState();

    //Objeto que armazena as teclas pressionadas atualmente

    Keys[] currentKeys = currentState.GetPressedKeys();

    //Se a tecla para cima estiver pressionada, a aceleração é acionada

    if (currentState.IsKeyDown(Keys.Up))

    {

        _copter.IsAccelerating = true;

    }

    else

    {

        _copter.IsAccelerating = false;

    }

    // Se o jogador pressionar ESC, o jogo será finalizado

    if (currentState.IsKeyDown(Keys.Escape))

        this.Exit();

}

Ao executar o projeto nesse momento, só poderemos sentir alguma mudança ao pressionar a tecla ESC do teclado, fazendo com que o jogo seja terminado. Para mover o helicóptero ainda precisamos do método de gerenciamento.

Nesse método (Listagem 8), a lógica utilizada é bem simples. Controlamos a aceleração que, se existente, é fixa como na vida real, mesmo sendo a do helicóptero ou da gravidade. Os principais cuidados que devemos ter nesse método são:

  • Impedir que o helicóptero “escape” da tela, tanto na parte superior, como a inferior;
  • Controlar a velocidade máxima do helicóptero, caso contrário, a sensação de subida do helicóptero não fica interessante;
  • Fazer com que o DestinationRectangle, e as áreas de colisão, sempre acompanhem os movimentos do helicóptero.

Esse método deve ser chamado dentro do método Update, do mesmo jeito que o ManageClouds e ManageArcs.

Listagem 8. Método ManageCopter.

private void ManageCopter()

{

 //Criação da instância de 2 objetos de apoio que, se não alterados, mantém os valores atuais do helicóptero

 Vector2 newPosition = new Vector2(_copter.Position.X, _copter.Position.Y);

 Vector2 newVelocity = new Vector2(_copter.Velocity.X, 0);

 //Se o helicóptero estiver sendo acelerado, e ainda não alcançou a altura máxima, continua a subir

 if ((_copter.Position.Y > 10) && (_copter.IsAccelerating))

 {

  //Definição da aceleração do helicóptero. É negativa devido ao eixo Y, que aumenta para baixo

  _copter.Aceleration = -0.3 f;

  //Definição da nova posição do helicóptero, somando-se a atual, com a velocidade e a aceleração

  newPosition.Y = _copter.Position.Y + _copter.Velocity.Y + _copter.Aceleration;

  //Condição que impede que a velocidade máxima seja ultrapassada.

  if (_copter.Velocity.Y > _copter.MaxVelocity)

  {

   newVelocity.Y = _copter.Velocity.Y + _copter.Aceleration;

  } else

   newVelocity.Y = _copter.MaxVelocity;

 }

 //Caso o helicóptero não esteja acelerando, essa parte de código impede que ele desça além do limite da tela
 else if ((_copter.Position.Y <= _CopterInitialPosition.Y - _copter.Image.Height) && !(_copter.IsAccelerating))

 {

  _copter.Aceleration = 0;

  newPosition.Y = _copter.Position.Y + _copter.Velocity.Y + 0.3 f;

  newVelocity.Y = _copter.Velocity.Y + 0.2 f;

 }

 //Quando o helicóptero encosta no limite inferior, a velocidade é zerada

 if (_copter.Position.Y > _CopterInitialPosition.Y - _copter.Image.Height)

 {

  newPosition.Y = _CopterInitialPosition.Y - _copter.Image.Height;

  newVelocity.Y = 0;

 }

 //Esse trecho evita com que o helicóptero avance a parte superior da tela

 if (_copter.Position.Y < _CopterInitialPosition.X)

 {

  newPosition.Y = _CopterInitialPosition.X;

  newVelocity.Y = 0;

 }

 //O helicóptero recebe os novos valores de velocidade e posição

 _copter.Velocity = newVelocity;

 _copter.Position = newPosition;

 //O DestinationRectangle e as áreas de colisão precisam sempre acompanhar a posição do helicóptero

 _copter.DestinationRectangle = new Rectangle((int) _copter.Position.X,

  (int) _copter.Position.Y,

  _copter.Image.Width / 3,

  _copter.Image.Height);

 _copter.ColisionArea[0] = new Rectangle((int) _copter.Position.X,

  (int) _copter.Position.Y,

  _copter.Image.Width / 3,

  _copter.Image.Height);

}

Execute o projeto novamente e veja que agora a tecla para cima, enquanto pressionada, faz o helicóptero subir na tela e permanecer no alto. Ao soltar, ele irá descer até o limite inferior.

Controle de colisões

No momento que estamos no desenvolvimento do jogo, não existe controle nenhum de colisão, tanto que o helicóptero acaba passando entre os arcos de qualquer forma, não sendo muito agradável.

Para este artigo, não iremos complicar muito, se o helicóptero bater nos limites do arco, este desaparece e o jogador perde uma vida. Caso contrário, se o helicóptero atravessar o arco por completo, a pontuação será incrementada.

O sistema de colisão já foi explicado na primeira parte do artigo, o que faremos agora é inserir um método que faça essa checagem, do helicóptero com cada arco, a cada atualização do jogo. Portanto, o método será chamado dentro do método Update. Este método de checagem é do tipo boolean, com o retorno servindo para diminuirmos ou não as vidas do jogador.

Na Listagem 9 acompanhe as variáveis que devem ser criadas na classe Game1, o estado atual do método Update, e por último o método de checagem de colisão.

Listagem 9. Controle de colisão e vidas do jogador.

//Variável para controle de vidas

int _lives;

//Variável para controle de pontos

int _score;

 

protected override void Update(GameTime gameTime)

{

    UpdateFrame((float)gameTime.ElapsedGameTime.TotalMilliseconds);

    ControlsManagement();

    ManageClouds();

    ManageArcs();

    ManageCopter();

    if (CheckObjectsColision())

    {

        _lives--;

    }

    // TODO: Add your update logic here           

    base.Update(gameTime);

}

 

private bool CheckObjectsColision()

{

    //Variável que retornará o valor

    bool Retval = false;

    //Laço que irá varrer cada arco, verificando a colisão com o helicóptero

    foreach (Objects.EnvironmentItem arc in _arc)

    {

        //Se houver colisão, o arco ficará invisível, até sair da tela e o retorno será true

        if ((arc.CheckColision(_copter.ColisionArea[0])) && (arc.IsVisible))

        {

            arc.IsVisible = false;

            Retval = true;

        }

    }

    return Retval;

}

Para fazer com que somente os arcos com a propriedade de visibilidade com valor verdadeiro sejam desenhados, precisamos alterar o método Draw, inserindo um simples controle de condição. Substitua as linhas indicadas na Listagem 10, inserindo a condição antes das chamadas ao método Draw do objeto spriteBatch, dentro do laço que varre cada uma das duas partes de cada arco.

Listagem 10. Atualizações do método Draw para ocultar os arcos.

//Substituir a linha

spriteBatch.Draw(_imgArcBack, _arc[i].Position, Color.White);

 

//Por esse trecho de código

if (_arc[i + 1].IsVisible)

{

    spriteBatch.Draw(_imgArcBack, _arc[i].Position, Color.White);

}

 

//Substituir a linha

spriteBatch.Draw(_imgArcFront, _arc[i + 1].Position, Color.White);

//Por esse trecho de código

if (_arc[i + 1].IsVisible)

{

    spriteBatch.Draw(_imgArcFront, _arc[i + 1].Position, Color.White);

}

Execute o jogo e veja que agora temos praticamente tudo pronto, se o helicóptero bate no arco, ele some e (sem aparecer ainda) o jogador perde uma vida. Portanto, a última parte do artigo é inserir o placar, fazendo a contagem e, finalizando o jogo quando as vidas forem todas perdidas.

Placar, vidas e “Game Over”

A exibição de textos nos jogos feitos com o XNA Framework é realizada utilizando-se um objeto nativo, o SpriteFont onde, através do Content Pipeline, podemos escolher uma fonte qualquer instalada na máquina, e utilizá-la no jogo. Para adicionar uma SpriteFont em nosso projeto, clique com o botão direito sobre o projeto Content, em seguida Add e New Item. Selecione SpriteFont (Figura 3) e deixe com o nome padrão, suficiente para nosso exemplo.

Inserindo uma SpriteFont em nosso projeto
Figura 3. Inserindo uma SpriteFont em nosso projeto.

Dê um duplo clique sobre o item criado, um arquivo XML será exibido. Nele podemos definir o nome da fonte que utilizaremos para esse objeto, troque o valor do campo FontName para Arial, que é a fonte que iremos usar. Devemos também criar um objeto deste tipo na classe Game1, para ser usado no jogo. Na Listagem 11, criamos o objeto e exibimos a versão final do método LoadContent, onde carregamos nossa fonte para a memória (última linha do método).

Listagem 11. Declaração da fonte, e método LoadContent.

SpriteFont _font;

 

protected override void LoadContent()

{

    // Create a new SpriteBatch, which can be used to draw textures.

    spriteBatch = new SpriteBatch(GraphicsDevice);

 

    // TODO: use this.Content to load your game content here

    _imgCopter = Content.Load<Texture2D>("Images\\Copter_SpriteTile");

    _imgCloud = Content.Load<Texture2D>("Images\\Clouds_SpriteTile");

    _imgArcBack = Content.Load<Texture2D>("Images\\Arc_Sprite_Back");

    _imgArcFront = Content.Load<Texture2D>("Images\\Arc_Sprite_Front");

    _font = Content.Load<SpriteFont>("SpriteFont1");

}

O próximo método que precisa ser criado, o UpdateScore (Listagem 12 junto com a versão final do método Update), é responsável por verificar, a cada atualização da tela, se o helicóptero passou por dentro de um arco, marcando aquele arco como responsável por marcar um ponto para o jogador.

Listagem 12. Versão final do método Update, e método UpdateScore.

protected override void Update(GameTime gameTime)

{

 UpdateFrame((float) gameTime.ElapsedGameTime.TotalMilliseconds);

 ControlsManagement();

 ManageClouds();

 ManageArcs();

 ManageCopter();

 if (CheckObjectsColision())

 {

  _lives--;

 } else if (UpdateScore())

 {

  _score++;

 }

 // TODO: Add your update logic here           

 base.Update(gameTime);

}



private bool UpdateScore()

{

 //Variável que retorna verdadeiro se existir pontuação a ser computada

 bool Retval = false;

 //Somente entramos na lógica, se os arcos já foram criados

 if (_arc.Count > 0)

 {

  foreach(Objects.EnvironmentItem arc in _arc)

  {

   //Para cada arco visível, ainda não marcado como pontuador, faz-se a verificação das áreas de colisão.

   if ((arc.ColisionArea.Count > 1) && (arc.IsVisible) && (arc.ScoreUpdate == false))

   {

    if ((arc.ColisionArea[0].Bottom < _copter.ColisionArea[0].Top) &&

     (arc.ColisionArea[1].Top > _copter.ColisionArea[0].Bottom) &&

     (arc.ColisionArea[0].Right + 10 < _copter.ColisionArea[0].Left) &&

     (arc.ColisionArea[0].Right < _copter.ColisionArea[0].Right))

    {

     //Arco sendo marcado como pontuador

     arc.ScoreUpdate = true;

     Retval = true;

    }

   }

  }

 }

 return Retval;

}

Antes de finalmente jogar os textos para a tela, vamos definir o número de vidas e a pontuação inicial, para isso defina o valor 5, para a variável _lives e 0 para a variável _score, junto a sua declaração. Agora vamos para o método Draw.

No caso de textos, o método do objeto spriteBatch a ser chamado deixa de ser o Draw e passa a ser chamado o DrawString, que possui alguns overloads, que no caso, utilizaremos o primeiro, contendo como parâmetros a variável que armazena nossa fonte, o texto a ser exibido, a posição e a cor.

A partir de agora, iremos envolver tudo que estávamos desenhando no método Draw da classe Game1, com uma condição que verifica se o número de vidas do jogador é maior do que 0, o que faz com que a mensagem “Game Over” seja exibida. Acompanhe os comentários da versão final deste método na Listagem 13, coloque o código no seu projeto e pronto, o jogo está finalmente com todos os elementos.

Listagem 13. Versão final do método Draw, já incluindo os textos da interface.

protected override void Draw(GameTime gameTime)

{

 graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

 spriteBatch.Begin();

 //Enquanto o jogador tiver vidas, os elementos serão desenhados na tela

 if (_lives > 0)

 {

  foreach(Objects.EnvironmentItem cloud in _cloud)

  {

   spriteBatch.Draw(_imgCloud, cloud.DestinationRectangle, cloud.SourceRectangle, Color.White);

  }

  for (int i = 0; i < _arc.Count - 1; i = i + 2)

  {

   //Condição para que o arco seja desenhado

   if (_arc[i + 1].IsVisible)

   {

    spriteBatch.Draw(_imgArcBack, _arc[i].Position, Color.White);

   }

  }

  spriteBatch.Draw(_copter.Image, _copter.DestinationRectangle, _copter.SourceRectangle,

   Color.White, 0, new Vector2(0, 0),

   SpriteEffects.None, 0);

  for (int i = 0; i < _arc.Count - 1; i = i + 2)

  {

   if (_arc[i + 1].IsVisible)

   {

    spriteBatch.Draw(_imgArcFront, _arc[i + 1].Position, Color.White);

   }

  }

  //Primeiro texto a ser escrito na tela

  spriteBatch.DrawString(_font, "Lives: " + _lives.ToString(), new Vector2(20, 20), Color.Black);

 } else

 {

  //Caso as vidas acabem, o texto de fim de jogo é exibido no meio da tela.

  string GameOver = "Game Over!!";

  //O método MeasureString da classe SpriteFonte, calcula o tamanho em pixels do texto, ajudando no posicionamento do mesmo

  float posX = graphics.GraphicsDevice.Viewport.Width / 2 - _font.MeasureString(GameOver).X / 2;

  float posY = graphics.GraphicsDevice.Viewport.Height / 2 - _font.MeasureString(GameOver).Y / 2;

  Vector2 position = new Vector2(posX, posY);

  //Chamado novamente o método DrawString, caso necessário

  spriteBatch.DrawString(_font, GameOver, position, Color.Black);

 }

 //O texto que apresenta a pontuação é sempre apresentado, mesmo ao fim do jogo

 spriteBatch.DrawString(_font, "Score: " + _score.ToString(), new Vector2(20, 40), Color.Black);

 spriteBatch.End();

 base.Draw(gameTime);

}

Quando o jogo é finalizado, devemos liberar da memória, todas as imagens que foram carregadas e que não mais serão necessárias. O método UnloadContent é responsável por esta tarefa. Portanto como conclusão do projeto, altere o método conforme a Listagem 14, onde cara imagem é marcada para liberação.

Listagem 14. Dispose das imagens utilizadas

protected override void UnloadContent()

{

 // TODO: Unload any non ContentManager content here

 _imgArcBack.Dispose();

 _imgArcFront.Dispose();

 _imgCloud.Dispose();

 _imgCopter.Dispose();

}

Isso conclui a construção do jogo. Você já pode rodá-lo e vê-lo funcionando por completo.

Conclusão

Com o final desta segunda parte do artigo, conseguimos apresentar os principais conceitos para que qualquer desenvolvedor consiga, utilizando o XNA Framework, criar e desenvolver jogos.

Fica o desafio para o leitor, em criar novos recursos para o jogo, por exemplo:

  • Fazer o helicóptero explodir quando tocar o arco – já conhecemos como fazer uma animação, assim como posicioná-la na tela;
  • Melhorar a animação do helicóptero que, na vida real, inclina-se para a frente quando a velocidade é aumentada – pode ser feito através de novos quadros na animação do helicóptero, ou através de alguns overloads para o método Draw do objeto spriteBatch;
  • Criar um histórico de pontuações – pode-se criar um placar na forma de XML, que ficará armazenado na máquina.

Para o desenvolvedor casual, ou estudante que queira se especializar nessa área, o XNA realmente veio para revolucionar, principalmente pelo recurso de possibilitar o desenvolvimento de jogos para o console Xbox 360, da Microsoft. Uma nova versão, ainda em Beta, está para ser lançada, permitindo o desenvolvimento de jogos para o tocador de mp3 portátil da empresa do Bill Gates, o Zune.

Confira também