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

Tutorial: XNA Invasores – Parte 9

Google News

Como prometido na última parte do tutorial, hoje vamos começar a implementar as colisões entre os objetos do jogo. Antes de fazer, vamos primeiro organizar melhor a classe JogoInvasores, implantando um sistema de estados para controlar o fluxo do jogo.
Vamos criar um novo arquivo com o nome EstadoJogo.cs e dentro dele adicionar o seguinte código:

namespace Tutorial_XNAInvasores
{
    public enum EstadoJogo
    {
        Menu, TelaDeJogo, GameOver, Encerrar, Invasao, Vitoria
    }
}

Este arquivo irá definir os possíveis estados do jogo: Menu (o jogo está apresentando o menu principal), TelaDeJogo (a tela principal do jogo), GameOver (o jogador perdeu), Encerrar ( o jogador pediu para sair do jogo), Invasao (as naves invadiram a área do jogador) e Vitoria (o jogador venceu).
Para começar a usar estes estados no jogo, primeiro é necessário declarar um atributo em JogoInvasores para armazenar o estado atual:

private EstadoJogo estado;

Feito isso, definimos o estado inicial do jogo no método Initialize.

Estado = EstadoJogo.Menu;

Repare que não estamos definindo o estado diretamente através do atributo, mas sim estamos usando uma propriedade “Estado”. Fazemos isso por que queremos que alguns métodos sejam chamados sempre que houver uma mudança de estado.
A propriedade “Estado” basicamente altera o estado e executa alguma ação extra dependendo do estado anterior.

private EstadoJogo Estado
{
    get { return estado; }
    set
    {
        switch (value)
        {
            case EstadoJogo.Menu:
                estado = value;
                break;
            case EstadoJogo.TelaDeJogo:
                estado = value;
                IniciarJogo();
                break;
            case EstadoJogo.GameOver:
                if (estado == EstadoJogo.TelaDeJogo)
                {
                    FinalizarJogo();
                }
                estado = value;
                break;
            case EstadoJogo.Invasao:
                if (estado == EstadoJogo.TelaDeJogo)
                {
                    FinalizarJogo();
                }
                estado = value;
                break;
            case EstadoJogo.Vitoria:
                if (estado == EstadoJogo.TelaDeJogo)
                {
                    FinalizarJogo();
                }
                estado = value;
                break;
            case EstadoJogo.Encerrar:
                estado = value;
                break;
        }
    }
}

Conforme você já deve ter reparado, esta propriedade faz chamadas a dois métodos que ainda não foram criados: IniciarJogo e FinalizarJogo. Estes são os métodos responsáveis por iniciar e finalizar a parte central do jogo.
Vamos então ver como fica IniciarJogo. Lembram que a gente adicionou os componentes NaveJogador e ControladorDeNaves dentro do método Update de JogoInvasor? Pois é, isto agora será feito por IniciarJogo, então podemos remover aquele código que marcamos como temporário. Para quem não se lembra, o código a ser removido é este:

    // Temporário: Cria e adiciona uma NaveJogador ao jogo
    if (naveJogador == null)
    {
        naveJogador = new NaveJogador(this, imagemJogador,
            new Vector2(300, 460));
        Components.Add(naveJogador);
    }
    // Temporário: Cria e adiciona um ControladorDeNaves ao jogo
    if (controladorDeNaves == null)
    {
        controladorDeNaves = new ControladorDeNaves(this);
        Components.Add(controladorDeNaves);
    }

IniciarJogo vai fazer praticamente a mesma coisa, com a diferença que não será preciso ficar verificando se o atributo é nulo (já que iremos chamar este método uma vez só, ao contrário de Update que era chamado várias vezes por segundo) e mais pra frente vamos instanciar também os escudos usando este método.

private void IniciarJogo()
{
    naveJogador = new NaveJogador(this, imagemJogador, new Vector2(300, 460));
    Components.Add(naveJogador);
    controladorDeNaves = new ControladorDeNaves(this);
    Components.Add(controladorDeNaves);
}

Para encerrar o jogo usamos o método FinalizarJogo. Nele, armazenamos a pontuação do jogador e depois removemos os componentes do jogo. A remoção dos componentes é uma tarefa muito simples, basta passar uma instância do componente para o método Components.Remove.
Novamente, neste método iremos remover também os escudos futuramente, mas por enquanto fica assim:

private void FinalizarJogo()
{
    ultimaPontuacao = naveJogador.Pontos;
    Components.Remove(naveJogador);
    naveJogador = null;
    Components.Remove(controladorDeNaves);
    controladorDeNaves = null;
}

O atributo ultimaPontuacao serve para armazenar a pontuação do jogador, mesmo depois que o componente for destruído. Ele é declarado assim:

private int ultimaPontuacao;

Agora voltamos ao método Update onde iremos definir o comportamento do jogo de acordo com seu estado atual. São três comportamentos distintos:

  • Se o estado for TelaDeJogo, chamamos o método AtualizarTelaDeJogo que contém a lógica central do jogo.
  • Se o estado for encerrar, finalizamos o jogo com o método Exit, da classe Game.
  • E se o estado for qualquer outro, chamamos o método ChecarOpcao que verifica a opção do usuário.
  • switch (estado)
    {
        case EstadoJogo.Menu:
        case EstadoJogo.GameOver:
        case EstadoJogo.Invasao:
        case EstadoJogo.Vitoria:
            ChecarOpcao();
            break;
        case EstadoJogo.TelaDeJogo:
            AtualizarTelaDeJogo();
            break;
        case EstadoJogo.Encerrar:
            Exit();
            break;
    }
    

    O método ChecarOpcao ficará responsável por verificar se o jogador deseja jogar novamente ou encerrar o jogo, mas nessa parte do tutorial não vamos nos preocupar com isto, portanto vamos apenas fazer com que este método altere o estado para “TelaDeJogo”.

    private void ChecarOpcao()
    {
        Estado = EstadoJogo.TelaDeJogo;
    }
    

    AtualizarTelaDeJogo é um método bem grande que verifica as colisões e checa o fim de jogo. Hoje vamos ver apenas algumas partes deste método. Já que ainda não estamos trabalhando com os escudos, algumas verificações ainda não serão feitas.
    Repare que vamos utilizar uma classe adicional, chamada GerenciadorDeColisao, para testar colisões entre os objetos do jogo. Esta é uma classe que contém apenas métodos estáticos e sua declaração será vista mais abaixo.

    private void AtualizarTelaDeJogo()
    {
    }

    Começamos o método testando se o tiro do jogador não é nulo e depois verificando sua colisão com as naves do ControladorDeNaves.

    if (naveJogador.Tiro != null)
    {
        if (GerenciadorDeColisao.ChecarColisao(controladorDeNaves,
            naveJogador.Tiro))
        {
            naveJogador.Pontos += 100;
            naveJogador.Tiro.Destruir = true;
        }
    }
    

    Depois fazemos o processo inverso, verificando a colisão do tiro das naves com a nave do jogador.

    if (controladorDeNaves.Tiro != null)
    {
        if (GerenciadorDeColisao.ChecarColisao(naveJogador,
            controladorDeNaves.Tiro))
        {
            naveJogador.Vidas--;
            controladorDeNaves.Tiro.Destruir = true;
        }
    }
    

    O resto do método diz respeito ao fim do jogo.

  • Se a quantidade de vidas restantes do jogador for menor que 0, mudamos o estado do jogo para “GameOver”.
  • Se as naves tiverem invadido o espaço do jogador, mudamos o estado para “Invasao”.
  • E se as naves inimigas acabaram, o jogador venceu e mudamos o estado para “Vitoria”.
  • if (naveJogador.Vidas < 0)
    {
        Estado = EstadoJogo.GameOver;
    }
    else if (controladorDeNaves.Invadiu)
    {
        Estado = EstadoJogo.Invasao;
    }
    else if (controladorDeNaves.QuantidadeDeNaves == 0)
    {
        Estado = EstadoJogo.Vitoria;
    }
    

    Por fim, se o jogador apertar ESC a partida atual termina com uma derrota. É claro que esta é uma opção muito simples, um aperfeiçoamento do jogo seria exibir uma tela de pause ou pelo menos de confirmação quando ESC for pressionado.

    if (Keyboard.GetState().IsKeyDown(Keys.Escape))
    {
        Estado = EstadoJogo.GameOver;
    }
    

    Agora já temos quase tudo pronto, falta apenas criar a classe GerenciadorDeColisao. Crie uma nova classe no projeto com este nome:

    namespace Tutorial_XNAInvasores
    {
        class GerenciadorDeColisao
        {
        }
    }
    

    Precisaremos de três versões diferentes do método VerificarColisao. A primeira irá verificar colisão entre as naves do ControladorDeNaves e um Tiro, a segunda irá testar se um Tiro colidiu com a NaveJogador e a última testa colisão entre um retângulo e um Tiro.
    Vamos à implementação do primeiro método:

    public static bool ChecarColisao(ControladorDeNaves controladorDeNaves,
        Tiro tiro)
    {
        NaveInvasor[,] naves = controladorDeNaves.Naves;
        for (int x = 0; x < ControladorDeNaves.NAVES_X; x++)
        {
            for (int y = 0; y < ControladorDeNaves.NAVES_Y; y++)
            {
                if (naves[x, y] != null)
                {
                    if (naves[x, y].ObterRetangulo().Intersects(
                        tiro.ObterRetangulo()))
                    {
                        naves[x, y] = null;
                        controladorDeNaves.QuantidadeDeNaves--;
                        return true;
                    }
                }
            }
        }
         return false;
    }
    

    Aqui estamos percorrendo todas as naves do ControladorDeNaves e verificando se elas não são nulas, ou seja, se já não foram destruídas. Caso não sejam, utilizamos o método Intersects da classe Rectangle para verificar se os retângulos da nave e do tiro se sobrepõem. Se isto acontecer, destruímos a nave em questão, reduzimos a contagem de naves restantes no controlador, e retornamos verdadeiro para que o jogo saiba que o tiro deve ser destruído e pontos devem ser adicionados ao total do jogador.
    A segunda implementação deste método verifica a colisão do Tiro dos invasores com a NaveJogador:

    public static bool ChecarColisao(NaveJogador naveJogador, Tiro tiro)
    {
        if (tiro.ObterRetangulo().Intersects(naveJogador.ObterRetangulo()))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    

    Temos um método bem mais simples aqui. Como há apenas uma NaveInvasor e um Tiro pra testar, não há necessidade de iterar por nenhuma lista. Apenas testamos então a interseção entre os retângulos dos dois objetos.
    Por fim, mais simples ainda, o teste que será utilizado futuramente para a colisão com os escudos:

    public static bool ChecarColisao(Rectangle caixa, Tiro tiro)
    {
        if (caixa.Intersects(tiro.ObterRetangulo()))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    

    Repare que o funcionamento é basicamente o mesmo da segunda versão, com a diferença que aqui recebemos diretamente uma instância de Rectangle, ao invés de obtê-lo dentro do método. Isto foi feito só para demonstrar algumas formas diferentes de trabalhar com a colisão.
    Para fechar, precisamos apenas adicionar umas propriedades em ControladorDeNaves para que seja possível acessar seus atributos “tiro”, “invadiu”, “quantidadeDeNaves” e “naves”. Feito isso, já podemos compilar e rodar o jogo.

    public Tiro Tiro
    {
        get { return tiro; }
    }
    
    public bool Invadiu
    {
        get { return invadiu; }
    }
    
    public int QuantidadeDeNaves
    {
        get { return quantidadeDeNaves; }
        set { quantidadeDeNaves = value; }
    }
    
    public NaveInvasor[,] Naves
    {
        get { return naves; }
    }
    

    Veja que agora você já pode destruir as naves inimigas e qualquer evento que leve ao fim do jogo (como destruir todos os inimigos ou pressionar ESC) simplesmente faz com que ele reinicie. É claro que vamos mudar isso em breve. Até a próxima.

    Não deixem de conferir o projeto completo aqui.

    12 comentários em “Tutorial: XNA Invasores – Parte 9”

    1. percebi que em alguns momentos o tiro do inimigo nao colide com a nave do jogador.

    2. Dã, tô vianjando. Parece que não colide mas na verdade está diminuindo a vida da nave.

    3. Achei muito bacana você tentar ensinar a programar jogos.
      Sou fã de todos os brasileiros que produzem software relacionado à Computação Gráfica, ao invés de serem apenas usuários dos programas ou bibliotecas que os gringos fazem.
      Acho que não se aplica a jogos diretamente, mas sei de um brasileiro que sabe muitíssimo a respeito da matemática e desenvolvimento na área de gráficos.
      Se quiser dar uma olhada no que o cara faz, está aqui: <a href="http://www.picturetopeople.org” target=”_blank”>www.picturetopeople.org
      Acho que vocês deveriam se unir e fazer softwares de última geração para mostrar ao mundo que nós não somos apenas consumidores.
      Abraço.

    4. Alguem me pode arranjar código para fazer um jogo em xna para entregar na escola, que é um jogo de uma nave que tem que andar de um ponto a outro a desviar-se de asteroides, se colidir com os asteroides tenho que diminuir ás vidas. Estou mesmo precisando de ajuda
      [email protected]

    5. Olá Diego, tudo beleza?
      Você tem planos para terminar o tutorial? Gostaria muito de ver este jogo concluído.
      Até mais…

      1. Oi, o problema é que eu ando sem tempo pra dar continuidade a este material. Com o trabalho e o mestrado, fica um pouco difícil.
        Mas eu tenho esse jogo finalizado, uma versão que eu montei antes de fazer o passo a passo para o tutorial. Vou ver se dou uma organizada e posto por aqui futuramente.

        1. Te entendo….
          Ontem comprei o livro XNA 3.0 para desenvolvimento de jogos no Windows, Zune e Xbox 360 (editora Brasport) e estou gostando.
          O seu tutorial tem um ritmo diferente. Apesar de não ficar entrando em detalhes mais profundos, ele explica muito bem os conceitos para o desenvolvimento do jogo (gostei muito do diagrama de classes, ajudou a entender muito o projeto do jogo).
          Bom, se você puder disponibilizar o código do jogo completo para estudarmos enquanto não completa o tutorial, agradeço muito.
          Até mais…

    6. Opa, é bom saber que o tutorial está ajudando. A idéia era essa mesmo, passar pelos principais conceitos para se criar um jogo simples, sem ser nem profundo nem superficial demais.
      Uma curiosidade: no CD que acompanha esse livro que você comprou tem um jogo meu. Ele se chama 'Paraquedismo' e foi feito para um concurso de jogos em 100 linhas de código.

    7. Sabe o que seria legal de aprender? Um pouco mais sobre colisões. Porque o método usado aqui é por retangulo, seria legal colisão de pixel!
      pergunta: eu tenho que decidir antes se vou fazer um jogo pra xbox ou pra pc?
      pergunta2: se eu fizer um jogo de dominó porém minhas peças tem relevo e sombra já deixa de ser 2D e passa a ser 3D?
      valeu pelo tutorial

    Não é possível comentar.