Pular para o conteúdo
Início » Gamedev » Tutorial: XNA Invasores – Parte 8

Tutorial: XNA Invasores – Parte 8

Google News

Hora de implementar as naves inimigas para que a gente possa treinar um pouco a pontaria. Vamos começar adicionando uma nova classe chamada NaveInvasor que herda da classe Nave, feita nas partes anteriores do tutorial.
Esta classe é bem simples. Precisamos apenas criar um construtor onde definiremos sua direção e velocidade inicial, e sobrescrever o método Update para dizermos como a nave deve ser atualizada.
Começamos com o construtor. Simplesmente definimos a direção inicial como Direita (o que significa que inicialmente todas as naves estarão se deslocando para a direita) e a velocidade de deslocamento como 20 (ou seja, as naves irão se mover 20 pixels por segundo na direção atual).

public NaveInvasor(JogoInvasores jogo, Texture2D imagem, Vector2 coord)
    : base(jogo, imagem, coord)
{
    direcao = Direcao.Direita;
    velocidade = 20.0f;
}

O método Update irá mover a nave na direção desejada, da mesma forma que foi feito com NaveJogador (na verdade, esta parte é igual nas duas classes e deveria ter aparecido na classe Nave, mas como isto só foi visto agora, vamos deixar assim mesmo).

public override void Update(GameTime gameTime)
{
    coord += new Vector2(velocidade * jogo.DeltaTempo * (int)direcao, 0);
    base.Update(gameTime);
}

Para finalizar esta classe, vamos criar um método que deve ser chamado quando uma das naves atinge um dos cantos da tela. Quando isto ocorre, todas as naves se deslocam um pouco para baixo e invertem sua direção.

public void InverterDirecao()
{
    coord += new Vector2(0, 10);
    if (direcao == Direcao.Direita)
    {
        direcao = Direcao.Esquerda;
    }
    else if (direcao == Direcao.Esquerda)
    {
        direcao = Direcao.Direita;
    }
}

Agora, para adicionar as naves invasoras no jogo, vamos criar um controlador que será responsável por criar, atualizar e destruir estas naves. Adicione uma nova classe no projeto com o nome de ControladorDeNaves e faça com que ela herde de DrawableGameComponent. Faremos isto, pois ControladorDeNaves é que será adicionado como um componente ao jogo, ao invés de adicionarmos cada NaveInvasor individualmente. Isto ajuda em obter um melhor desempenho, uma vez que o XNA não precisará cuidar de dezenas de componentes.
Criada a classe, adicionamos seus atributos. Como sempre, começamos com uma instância de JogoInvasores:

private JogoInvasores jogo;

Em seguida criamos uma matriz que irá conter as naves criadas pelo controlador. Para quem não está muito familiarizado com o C#, é possível fazer a declaração de uma matriz bidimensional sem especificar o tamanho de nenhuma de suas dimensões escrevendo “[,]” após o tipo da matriz. Faremos isto, pois os limites da matriz serão definidos mais à frente, quando criarmos as naves.

private NaveInvasor[,] naves;

Duas texturas são necessárias para NaveInvasor: a textura da própria nave e a textura do tiro da nave. Para armazenar estas texturas, criamos mais dois atributos no controlador:

private Texture2D imagemNave;
private Texture2D imagemTiro;

O controlador de naves será responsável por verificar se uma nave atingiu os limites da tela e ordenar a todas elas que invertam sua direção. Para fazer este controle, utilizamos um atributo que diz se as naves devem ou não inverter sua direção.

private bool inverterDirecao;

Utilizamos uma variável para contar o tempo que falta para que as naves possam atirar, impedindo que elas fiquem atirando direto, sem nenhum intervalo.

private float tempoParaAtirar;

Quando for permitido que as naves atirem (ou seja, o tempo para atirar tiver passado) faremos um teste baseado num número aleatório para definir qual nave irá atirar (veremos isto mais abaixo). Para gerar números aleatório, vamos criar uma instância da classe Random provida pelo .Net Framework justamente para lidar com números randômicos.

private Random random;

Finalizando a parte dos tiros, quando um for disparado precisaremos armazenar uma nova instância de Tiro, então criaremos um atributo para isto. Assim como foi feito com NaveJogador, apenas um tiro por vez poderá separado, portanto não há necessidade de se criar um vetor ou uma lista, apenas uma variável simples já basta.

private Tiro tiro;

Durante sua execução, o Jogo pode acabar de várias formas, mas apenas duas delas estão diretamente relacionadas com as naves invasoras: todas as naves foram destruídas (vitória do jogador) ou as naves ultrapassaram um limite e invadiram a defesa (derrota do jogador). Para controlar isto, teremos um atributo que conta a quantidade de naves restantes no Jogo e um outro que indica se alguma nave conseguiu invadir a área do jogador.

private int quantidadeDeNaves;
private bool invadiu;

Para terminar os atributos de ControladorDeNaves, vamos adicionar duas constantes que irá definir a quantidade de naves no eixo X e Y.

public const int NAVES_X = 10;
public const int NAVES_Y = 5;

Agora criamos o construtor. Primeiramente armazenamos a instância de JogoInvasores, definimos o atraso inicial do primeiro tiro (tempoParaAtirar), instanciamos o gerador de números aleatórios e definimos tiro como null (isto é importante, pois um dos requisitos para a criação de um novo tiro é que este atributo seja nulo, o que significa que não existe outro tiro na tela disparado por uma nave invasora).

public ControladorDeNaves(JogoInvasores jogo)
    : base(jogo)
{
    this.jogo = jogo;
    tempoParaAtirar = 2.0f;
    random = new Random();
    tiro = null;

E depois finalizamos definindo a quantidade inicial de naves como sendo NAVES_X * NAVES_Y, ou seja, cinqüenta naves, e dizendo ao jogo que a invasão ainda não ocorreu (invadiu = false).

quantidadeDeNaves = NAVES_X * NAVES_Y;
invadiu = false;

Continuando, vamos carregar as texturas da nave e do tiro sobrescrevendo o método LoadContent do ControladorDeNaves. Perceba que desta vez vamos carregar as texturas diretamente no componente, ao invés de carregá-lo em JogoInvasores e passar para o ControladorDeNaves como parâmetro. Fizemos isto porque o jogo terá um único controlador de naves, o que significa que uma mesma textura não será carregada várias vezes, portanto podemos deixar que o próprio controlador carregue sua textura.
Uma vez que a textura da nave já foi carregada, podemos então instanciar as naves do jogo. Para isso, vamos fazer uma chamada ao método CriarNaves dentro de LoadContent, fazendo com que as naves sejam criadas imediatamente após o carregamento das texturas.

protected override void LoadContent()
{
    imagemNave = jogo.Content.Load("imagemNaveInvasor");
    imagemTiro = jogo.Content.Load("imagemTiroJogador");
    CriarNaves();
    base.LoadContent();
}

Ok, estamos carregando as imagems, mas para que elas possam realmente ser carregadas precisamos adicioná-las ao projeto. Vá em Add -> Existing Item… e adicione estes dois arquivos ao seu projeto: imagemTiroInvasor.png e imagemNaveInvasor.
A implementação do método CriarNaves fica da seguinte forma: alocamos memória para a matriz que tinha sido declarada anteriormente e depois fazemos um laço por todos os elementos desta matriz criando novas instâncias de NaveInvasor, passando suas coordenadas e a imagem.
O método Update é bem grandinho, então vamos por partes para não confundir. Primeiramente, atualizamos o tempoParaAtirar diminuindo seu valor de acordo com o deltaTempo.

public override void Update(GameTime gameTime)
{
    tempoParaAtirar -= jogo.DeltaTempo;

Depois, fazemos um laço por todas as naves do controlador usando a palavra-chave do C# “foreach”. Dentro deste laço vamos manipular cada uma das naves contidas na matriz naves.
Antes de mais nada, verificamos se a nave não é nula. Caso seja nula, nada é feito. Caso não seja, chamamos o método Update da nave atual.

if (nave != null)
{
    nave.Update(gameTime);

Agora verificamos se a nave chegou perto demais de um dos lados da tela e ativar o flag inverterDirecao caso isto ocorra. No nosso caso, consideramos que uma nave chegou perto demais de um dos lados se ela estiver a menos de 10 pixels do canto da tela.

if (nave.Coord.X < 10 && nave.Direcao == Direcao.Esquerda)
{
    inverterDirecao = true;
}
else if (nave.Coord.X + imagemNave.Width > JogoInvasores.LARGURA - 10
         && nave.Direcao == Direcao.Direita)
{
    inverterDirecao = true;
}

Na sequencia verificamos se a nave invadiu o espaço do jogador, o que aqui significa ter sua coordenada Y maior que 340.

if (nave.Coord.Y > 340)
{
    invadiu = true;
}

Para finalizar este laço, testamos se o tempo para atirar já passou e se não há nenhum outro tiro na tela (tiro == null). Em caso positivo, geramos um número aleatório entre 0 e 1000 e verificamos se ele é menor que 30. Se for, um novo tiro é criado logo abaixo da nave atual. Este teste com o número aleatório existe para permitir que todas as naves atirem. Quando um novo tiro é criado, não podemos esquecer de reinciar o contador tempoParaAtirar.

if (tempoParaAtirar < 0.0f && tiro == null)
{
    if (random.Next(1000) < 30)
    {
        tiro = new Tiro(jogo, imagemTiro, new Vector2(
            nave.Coord.X + imagemTiro.Width / 2, nave.Coord.Y),
            200.0f);
        tempoParaAtirar = 2.0f;
     }
}

Antes de prosseguir, vamos fechar dois blocos de código que ainda estão abertos.

    }
}

Fechamos estes blocos mas ainda há coisas a serem feitas em Update. Primeiramente, caso o flag inverter direção esteja ativo, vamos fazer uma nova iteração por todas as naves chamando seu método inverterDirecao. Estamos fazendo este novo laço ao invés de fazê-lo dentro do laço anterior para garantir que todas as naves tenham sua direção invertida. Caso isso fosse feito diretamente no outro laço, as naves atualizadas antes da ativação do flag inverterDirecao não seriam afetadas pelo flag.

if (inverterDirecao)
{
    foreach (NaveInvasor nave in naves)
    {
        if (nave != null)
        {
            nave.InverterDirecao();
        }
    }
        inverterDirecao = false;
}

Por fim, caso um tiro tenha sido disparado, ele é atualizado. Se o tiro tiver ativado seu flag destruir (ao sair da tela ou atingir um objeto) atribuímos null a ele e o removemos da memória.

if (tiro != null)
{
    tiro.Update(gameTime);
    if (tiro.Destruir)
    {
        tiro = null;
    }
}

Terminamos o método Update chamando base.Update e fechando as últimas chaves abertas.

base.Update(gameTime);
}

O método Draw é muito simples. Primeiramente iteramos por todas as naves e verificamos se a nave corrente é nula, e caso não seja, a desenhamos. Depois terminamos o método desenhando o tiro caso ele não seja nulo.

public override void Draw(GameTime gameTime)
{
    foreach (NaveInvasor nave in naves)
    {
        if (nave != null)
        {
            nave.Draw(gameTime);
        }
    }
    if (tiro != null)
    {
        tiro.Draw(gameTime);
    }
    base.Update(gameTime);
}

Para adicionar o controlador de naves ao jogo e permitir que as naves apareçam na tela, vamos usar o mesmo processo que fizemos antes com NaveJogador. Iremos criar uma variável controladorDeNaves e instanciá-lo temporariamente no método Update de JogoInvasores. Mais à frente quando implementarmos o sistema de estados do Jogo, a criação de controladorDeNaves irá aparecer em um lugar mais conviniente.
Declare a variável dentro da classe JogoInvasores da seguinte forma:

private ControladorDeNaves controladorDeNaves;

Depois, adicione o seguinte trecho de código no método Update.

// Temporário: Cria e adiciona um ControladorDeNaves ao jogo
if (controladorDeNaves == null)
{
    controladorDeNaves = new ControladorDeNaves(this);
    Components.Add(controladorDeNaves);
}

Agora já temos as naves inimigas voando pelo cenário e atirando contra o jogador. Mas por hora nenhuma colisão está sendo feita e os tiros irão simplesmente passar direto pelos personagens. Pior que isso, nenhuma verificação está sendo feita para determinar se o jogo acabou, portanto se você deixar o jogo rodando por um tempo as naves eventualmente irão sair pela parte de baixo da tela.
Nas próximas partes tanto os cálculos de colisão quanto as verificação de fim de jogo serão adicionadas. Enquanto isso, vocês podem testar o jogo baixando o projeto aqui.
Até mais.

Jogo com as Naves Invasoras!

4 comentários em “Tutorial: XNA Invasores – Parte 8”

  1. Ilair dos Santos

    Olá!
    Quando sairá o restante com cálculos, estou ansioso para continuar e aprender mais

  2. O seguinte trecho
    "public override void Update(GameTime gameTime)
    {
    coord += new Vector2(velocidade * jogo.DeltaTempo * (int)direcao, 0);
    base.Update(gameTime);
    }"
    está errado, o correto seria ((velocidade+jogo.DeltaTempo)*(int)direcao,0);

  3. Não Grivos, o trecho de código está correto.
    Explicando: a idéia é fazer com que a velocidade de movimento em um quadro seja proporcional à fração de tempo que aquele quadro levou para ser renderizado. Se um quadro demorou mais tempo, significa que o fps está baixo e o movimento naquele quadro terá um salto maior. Se o fps estiver alto, cada quadro executará um movimento menor, resultando em uma animação mais fluída.
    Entendido?

Não é possível comentar.