Tutorial: XNA Invasores - Parte 5
Início » Gamedev » Tutorial: XNA Invasores – Parte 5
Tutorial: XNA Invasores – Parte 5
Na 5º parte do nosso tutorial vamos ver o funcionamento dos Game Components do XNA, uma adição simples mas que ajuda muito na reutilização de código.
Antes de começar pra valer, vamos ver uma coisinha que ficou faltando na última parte mas é muito importante: a taxa de quadros por segundo do jogo. O XNA tem duas formas de lidar com a velocidade do jogo, temos uma taxa fixa ou variável.
Por padrão, o jogo sempre trabalha com uma taxa fixa de 60 quadros por segundo, ou qualquer outro valor definido através da propriedade TargetElapsedTime. Isto é muito interessante quando se faz jogos pro XBOX 360, já que como o hardware é padrão, temos a garantia de que o jogo irá se comportar da mesma forma em todos os videogames.
No caso dos computadores isto é um grande problema, uma vez que existem inúmeras configurações diferentes e dificilmente o jogo irá se comportar igualmente em duas máquinas distintas. Então, ao invés de fixar o número de quadros por segundo, podemos deixar essa taxa variável e fazer todos os cálculos dependentes do tempo através do parâmetro gameTime que os métodos Update e Draw recebem (veremos isto à frente). Para deixar a taxa variável, apenas adicione a seguinte linha no construtor de JogoInvasores:
Agora vamos criar a classe mais primitiva do nosso modelo de classes que será herdada por diversas outras classes. Estamos falando, é claro, da classe Entidade. Como veremos, Entidade é um Game Component, mas o que é um Game Component? Você deve estar se perguntando.
Lembram quando a gente definiu nosso jogo, que herdava da classe Game e com isso vários métodos eram chamados automaticamente? Pois então, os Game Components funcionam da mesma forma. Nós vamos definir um Game Component e adicioná-lo ao jogo, depois disso o próprio jogo se encarrega de cuidar dele e chamar seus métodos.
Existem dois tipos de Game Components no XNA: o GameComponent, que é um componente comum que NÃO executa operações de desenho e o DrawableGameComponent que possui um método Draw e pode ser desenhado. No nosso caso, iremos usar um DrawableGameComponent.
Chega de enrolar e vamos criar então a classe Entidade. Com o botão direito sobre o nome do projeto, escolha Add -> New Item. O Visual C# nos dá um modelo chamado GameComponent que já traz a implementação de alguns métodos desta classe, mas como teríamos que fazer muitas alterações neste arquivo (especialmente por não haver um modelo de DrawableGameComponent), escolha Code File no diálogo que se abriu e dê a ele o nome de Entidade.cs. Futuramente, quando for dito para criar um novo arquivo em branco, este é processo a ser feito.
No novo arquivo criado, vamos fazer referência ao Namespace Microsoft.Xna.Framework que é onde estão definidas várias classes usadas neste arquivo. Aproveitando, crie o Namespace Tutorial_XNAInvasores neste arquivo também (pra quem não se lembra, é neste Namespace que estamos criando nosso jogo).
Dentro do Namespace Tutorial_XNAInvasores, vamos criar a classe Entidade (declarada como abstrata, já que uma Entidade não pode ser instanciada). Como já foi dito, esta classe herda de DrawableGameComponent e com isso possui vários métodos que podem ser sobrescritos. Porém, Entidade também será herdada por várias outras classes do nosso modelo e estas classes terão suas próprias implementações dos métodos de DrawableGameComponent, portanto aqui faremos apenas a parte da herança e deixaremos para sobrescrever os métodos mais tarde.
public abstract class Entidade : DrawableGameComponent
Com isto já temos a definição da classe, hora então de definir seus atributos. No modelo, definimos que esta classe teria apenas dois atributos: suas coordenadas X e Y, como dois valores do tipo float. Ao invés de deixarmos estes valores como duas variáveis separadas, vamos usar a classe Vector2 do XNA que define um vetor 2D, que contém uma coordenada X e uma coordenada Y como precisamos, com a vantagem de que muitos métodos do XNA trabalham com instâncias de Vector2, ao invés de valores X e Y separados.
Para que as coordenas da Entidade possam ser acessadas externamente, seguindo os conceitos de orientação a objetos, o atributo não pode ser público. Precisamos portanto criar métodos de acesso. No C#, temos a facilidade de poder usar propriedades que encapsulam métodos de leitura (get) e escrita (set) em um só. Funciona da mesma forma que seria feito em Java, por exemplo, mas ao invés de termos getValor() e setValor(), temos uma propriedade única que já resolve isso pra gente.
Vamos nomear então o vetor com as coordenadas de XY de coord, deixando ele como protegido para que possa ser acessado livremente pelas classes que o herdam:
protected Vector2 coord;
Na seqüência, criamos a propriedade de acesso ao atributo coord. O método definido por get será chamado sempre que quisermos ler o valor do atributo. Em set, definimos o que será executado ao se escrever um valor em coord.
public Vector2 Coord
get { return coord; }
set { coord = value; }
Todo GameComponent no XNA tem uma referência para o jogo que o instanciou. Esta referência deve ser passada no seu construtor. No nosso caso, além de passar a referência ao jogo para o construtor, também vamos armazenar esta referência num atributo para que mais à frente possamos acessar algumas propriedades do jogo que serão importantes para as Entidades. Para isso, crie o atributo jogo nesta classe.
protected JogoInvasores jogo;
Pra finalizar a classe Entidade, falta apenas definir seu construtor. O único parâmetro recebido aqui será uma instância de jogo. Utilizando a palavra chave base, repassamos o jogo recebido para o construtor de DrawableGameComponente. Dentro do nosso construtor, vamos armazenar esta instância no atributo jogo e definir a posição inicial da Entidade como sendo (0, 0), através da propriedade Zero de Vector2 que retorna um vetor com as coordenadas (0, 0).
public Entidade(JogoInvasores jogo) : base(jogo)
coord = Vector2.Zero;
Seguindo em frente, podemos criar a classe Nave, que é a próxima na hierarquia, mas primeiro vamos criar dois tipos enumerados: EstadoNave (que irá definir os possíveis estados para as naves no jogo) e Direcao (que define as possíveis direções nas quais uma nave pode se movimentar).
O processo é o mesmo da criação de uma classe, crie um arquivo vazio de nome EstadoNave.cs e adicione o seguinte código:
public enum EstadoNave
Ativo, Inativo, Destruido
Repita os mesmos passos para a criação do arquivo Direcao.cs e cole o a seguir código nele. Veja que aqui definimos valores inteiros para cada valor do enum, isto é importante porque utilizaremos estes valores na hora de mover as naves.
Esquerda = -1,
Direita = 1,
Agora sim podemos criar nossa nave. Uma nave em si também é uma classe abstrata, visto que não estaremos criando instancias desta classe, mas sim de suas derivações (NaveJogador, NaveInvasor e NaveBonus). Esta classe irá trazer alguns atributos comuns aos diversos tipos de nave, e também irá implementar o método Draw herdado de DrawableGameComponent (através da herança da classe Entidade), uma vez que todas as naves se desenham da mesma forma. Porém, o método Update ainda não será implementado aqui, já que cada tipo de nave terá um comportamento diferente do outro.
Começamos de novo com a adição dos Namespaces, que desta vez são dois. Além de Microsoft.Xna.Framework, precisamos adicionar Microsoft.Xna.Framework.Graphics pois estaremos trabalhando com a classe Texture2D deste Namespace.
Vamos criar a classe Nave e fazê-la herdar da classe Entidade. Lembre-se que Nave também é uma classe abstrata.
public abstract class Nave : Entidade
Teremos quatro atributos nesta classe: estado (define o estado da nave), velocidade (a velocidade de movimento dela), direcao (a direção atual da nave) e imagem (uma textura que representa a nave na tela).
protected EstadoNave estado;
protected float velocidade;
protected Direcao direcao;
protected Texture2D imagem;
Inicialmente, o estado de todas as naves será o mesmo: EstadoNave.Ativo. A imagem e as coordendas iniciais (herdada de Entidade) serão definidas pelo jogo no momento da criação da Nave, e os outros atributos serão definidos em cada tipo de Nave mais tarde. O construtor fica assim:
public Nave(JogoInvasores jogo, Texture2D imagem, Vector2 coord)
: base(jogo)
estado = EstadoNave.Ativo;
Dos atributos criados, o único que precisa ser visto externamente é direcao. Para isso, vamos criar uma propriedade Direcao. Como precisamos apenas ler a direção da Nave, vamos criar apenas com o método get, sem o método set.
Obs.: Não tem nenhum problema o tipo de retorno ter o mesmo nome da propriedade porque em tempo de compilação o C# cria um método de nome get_Direcao que é chamado quando usamos esta propriedade, portanto não há conflito de nomes.
public Direcao Direcao
get { return direcao; }
Além da direção, precisaremos também obter uma caixa ao redor da Nave para calcular sua colisão com outros objetos. Para fazer isto, criamos um método que calcula esta caixa com base nas coordenadas atuais e nas dimensões da imagem da Nave. Isto é feito criando uma instancia da classe Reclangle (provida pelo XNA), com as coordenadas XY da Nave (que precisam ser convertidas para int, já que o C# não faz conversões implícitas) e a largura e altura da imagem, obtida através das propriedades Width e Height da classe Texture2D.
return new Rectangle((int)coord.X, (int)coord.Y, imagem.Width,
imagem.Height);
Para terminar, falta apenas implementar o método Draw, que é responsável por desenhar as naves. Mas antes disso, precisamos adicionar uma propriedade em JogoInvasores para que seja possível acessar seu SpriteBatch e assim efetuar desenhos na tela. Adicione o seguinte código em JogoInvasores:
Voltando a mexer com a classe Nave, criamos o método Draw. Este é um método herdado de DrawableGameComponent que recebe como parâmetro uma instancia de GameTime, caso seja necessário basear os desenhos em alguma contagem de tempo.
O que fazemos aqui é simples: primeiro verificamos se a imagem não é nula (apenas para garantir que o programa não vai dar erro caso a imagem não tenha sido carregada com sucesso) e depois efetuamos o desenho em si. Todas as operações de desenho 2D no XNA iniciam com SpriteBatch.Begin() e finalizam com SpriteBatch.End(). Tudo o que será desenhado vai entre estes dois métodos.
No caso da Nave, a única chamada necessária é para o método SpriteBatch.Draw(), que recebe como parâmetros a imagem a ser desenhada (o Texture2D recebido no construtor), as coordenadas em que a imagem deve aparecer (um Vector2) e uma cor que pode alterar a coloração da imagem. Para desenhar a imagem sem nenhum efeito, usamos Color.White.
Com isto terminamos mais uma parte do tutorial. Hoje vimos vários conceitos importantes do XNA, mas o executável gerado pelo projeto vai trazer o mesmo resultado da última parte, já que nenhuma instância de classe foi criada e nenhuma alteração mais significativa no jogo foi feita.