Archive

Posts Tagged ‘Tutoriais’

Tutorial: Pixel Shader no XNA

O pipeline programável permite substituir etapas do pipeline gráfico (uma série de passos feitas por uma API gráfica para renderizar objetos gráficos) por um código personalizado. Pode-se alterar tanto o código de processamento de vértices (vertex shader) quanto de pixels (pixel shader).

Este código é executado na GPU e possibilita, por exemplo, alterar o modelo de iluminação de uma malha tridimensional. Assim, é possível obter materiais distintos e fazer com que uma roupa tenha uma aparência diferente da pele do personagem, algo que não era possível no pipeline fixo.

Uma grande vantagem da utilização de shaders é que sua execução é feita diretamente em GPU. Além das placas gráficas terem diversas operações comuns à computação gráfica (como soma de vetores e matrizes) implementadas diretamente em hardware, as GPUs mais recentes possuem diversos processadores paralelos que permitem processar vários vértices ou fragmentos ao mesmo tempo, tornando o processo ainda mais rápido.

Em um programa tridimensional, geralmente se usa uma combinação de um vertex shader e um pixel shader. Primeiramente os vértices da malha dos objetos 3D são processados pelo vertex shader e cada pixel que compõe a superfície destes objetos é processado pelo pixel shader. Neste tutorial eu vou focar somente no pixel shader, portanto o processamento será feito sobre imagens bidimensionais. Este tipo de shader é útil tanto em jogos 2D quanto para aplicar efeitos de pós-processamento em jogos 3D, onde primeiro o jogo é renderizado para uma textura e depois o processamento é feito sobre esta textura para só então o resultado final ser mostrado na tela.

No passado, os shaders eram programados em assembly, mas depois surgiram linguagens parecidas com o C, como o HLSL (usado no DirectX e XNA, GLSL (usado no OpenGL) e o CG. Os exemplos mostrados aqui foram desenvolvidos em HLSL, mas a sintaxe destas linguagens é bem parecida e traduzir de uma para outra não é muito complicado.

Utilizar um pixel shader no XNA é um processo bem simples. Na verdade, quando usamos a classe SpriteBatch já estamos usando um shader (o XNA não aceita o pipeline fixo por motivo de compatibilidade com o Xbox 360), mas esta classe nos permite abstrair seu uso. Aqui nós continuaremos usando o SpriteBatch, aplicando o shader sobre ele.

Para começar, podemos criar um novo shader no Visual Studio. Isto é efeito com um clique direito no sub-projeto Content e selecionando Add -> New Item -> Effect File. O modelo fornecido pelo XNA oferece um pixel e um vertex shader prontos. Caso alguém tenha a curiosidade de aplicá-lo a um objeto, ele irá projetar o modelo e alterar sua cor para vermelho.

Veremos dois exemplos aqui. O primeiro deles irá inverter as cores da textura e o outro irá convertê-la para escala de cinza. Em ambos os casos não usaremos o vertex shader. Para não confundir, vamos apagar o código padrão do XNA e começar nosso shader do zero.

O primeiro passo é criar uma estrutura de entrada para o pixel shader. Esta estrutura é passada automaticamente pelo vertex shader e os valores devem possuir uma semântica para que o programa saiba interpretá-los corretamente. A semântica pode ser uma coordenada de textura, cor, posição, etc. Aqui vamos precisar somente da cor e da coordenada de textura. A cor é aquele valor passado no SpriteBatch (geralmente branco) e a coordenada de textura é a coordenada a imagem referente a cada pixel desenhado na tela.

struct PixelShaderInput
{
    float4 Color : COLOR;
    float2 TextureCoord : TEXCOORD0;
};

Também é necessário declarar uma textura e um sampler (amostrador) de textura usado pelo SpriteBatch para acessar a imagem. A textura é declarada como extern porque ela vai ser declarada de fato no SpriteBatch. O TextureSampler permite acessar os texels da textura em questão.

uniform extern texture InputTexture;	

sampler TextureSampler = sampler_state
{
	Texture = < InputTexture > ;
};

Feito isso, criamos nossa função de pixel. Basta escrever uma função que receba a estrutura criada acima como parâmetro. Esta função deve retornar um float4, que é um vetor de quatro posições usado para representar uma cor RGBA. Isto porque, na verdade, depois desta etapa o pixel nada mais é que uma cor. Repare a semântica da função (COLOR0) indicando que o retorno é uma cor.

float4 PixelShaderFunction(PixelShaderInput input) : COLOR0
{
    // TODO: add your pixel shader code here.
}

Agora podemos fazer um cálculo que será executado pelo pixel shader. No primeiro exemplo, valor simplesmente inverter a cor do pixel. Para isso, primeiro acessamos o pixel usando a função text2D (usando o amostrador da textura e a coordenada de textura, sendo que a coordenada é gerada automaticamente pelo pixel shader) e depois multiplicamos pela cor passada pelo SpriteBatch. Enfim, fazemos 1 – a cor para obter seu valor invertido (internamente será gerado um float4 com o resultado de 1 – r, 1 – g, 1 – b e 1 – a) e retornamos o resultado (que é a cor final do pixel).

float4 PixelShaderFunction(PixelShaderInput input) : COLOR0
{
    // TODO: add your pixel shader code here.
    input.Color = tex2D(TextureSampler, input.TextureCoord) * input.Color;
    input.Color = 1 - input.Color;
    return input.Color;
}

Em um arquivo de efeito como este é possível ter diversas funções diferentes, da mesma forma que um programa em C normal. E assim como um programa em C é preciso indicar qual o ponto de partida do programa (o equivalente à função main). Um shader também pode ser dividido em diversas técnicas e passos, mas não vamos entrar em detalhes sobre isto agora. A linha abaixo indica que nossa função será usada como o pixel shader para o passo um da técnica um.

technique Technique1
{
    pass Pass1
    {
        // TODO: set renderstates here.
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

Perceba que deve-se especificar a versão do pixel shader usado. Versões mais altas possuem mais recursos, tanto em variedade de funções quanto em limites do que se pode fazer, mas só rodarão em placas que suportem o mínimo exigido.

Pronto, com este shader bastante simples já é possível inverter a cor de uma textura. Agora precisamos aplicar o shader no XNA. Não vou explicar aqui com carregar e desenhar as imagens, qualquer dúvida vocês podem ver os artigos anteriores ou perguntar nos comentários. Carregar um efeito é praticamente igual a carregar uma textura, primeiro uma variável do tipo Effect é declarada e depois o efeito é carregado no método LoadContent.

Effect efeitoInverso;

protected override void LoadContent()
{
     spriteBatch = new SpriteBatch(GraphicsDevice);
     textura = Content.Load < Texture2D > ("homer");
     efeitoInverso = Content.Load < Effect > ("EfeitoInverso");
}

Na hora de desenhar, também não há complicação. Primeiro é preciso iniciar o efeito e depois iniciar o passo desejado (caso haja mais de um passo). Feito isso as texturas são desenhadas normalmente usando SpriteBatch.Draw e deve-se finalizar o processo fechando o passo e o efeito.

spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate,
    SaveStateMode.None);
efeitoInverso.Begin();
efeitoInverso.CurrentTechnique.Passes[0].Begin();
spriteBatch.Draw(textura, new Vector2(50, 50), Color.White);
efeitoInverso.CurrentTechnique.Passes[0].End();
efeitoInverso.End();
spriteBatch.End();

O shader da escala de cinza vai mudar muito pouco. Na hora de calcular a cor do pixel, ao invés de fazer 1 – a cor, vamos multiplicar cada componente por um peso (relacionado à sensibilidade do olho humano aos canais RGB). As componentes de um vetor podem ser acessados tanto pelo formato .xyzw quanto .rgba, como eu usei abaixo.

input.Color = input.Color.r * 0.3 + input.Color.g * 0.59 + input.Color.b * 0.11;

Segue uma imagem do resultado final:

tutorial_pixelshader

E é isto. Neste tutorial aprendemos a criar um pixel shader bem simples e aplicá-lo a uma textura no XNA. A idéia aqui foi fazer apenas uma introdução ao tema, visto que muito mais coisas podem ser feitas utilizando este recurso. Existem outras formas de se implementar estes exemplos, eu fiz da forma que me sinto mais confortável.

O projeto com os dois shaders e o código-fonte pode ser baixado aqui.

Um bom próximo passo para quem se interessou pelo assunto é pesquisar sobre como passar parâmetros da aplicação para o shader.

Diego Barboza Artigos, Tutoriais, XNA , , ,

Criando um jogo da memória em Java – Parte 01

Apesar de ter dito no meu blog pessoal que eu estava interessado em C# e Flash (ainda estou), vira e mexe surge na minha mente que eu tenho de criar um jogo da memória em Java. Talvez pela linguagem ser melhor do que o Visual Basic e para treinar um pouco eu decidi tentar. Obviamente eu ainda não comecei, mas optei por ir postando o andamento da construção desse jogo aqui no GamedevBR, com artigos técnicos. Além disso, como vocês tem mais experiência do que eu, vocês podem até me ajudar com dicas que outros leitores podem aproveitar também!

Tá, e se você ainda não começou, pra quê este texto então? Simples: pra começar a citar os pré-requisitos para criar um game desse tipo. Um erro clássico de quem pensa em programar algo é que ninguém planeja e já sai criando. Sei que para um game de memória não é necessário ter um planejamento elaborado, mas para quem pensa em seguir na área de gamedev, ter pelo menos um rascunho é essencial pro desenvolvimento não se perder no meio do caminho.

Read more…

Rodrigo Flausino Java, Tutoriais , , ,

Tocando músicas e efeitos sonoros no XNA 3.0

Uma das grandes adições do XNA 3.0 foi a possibilidade de se tocar músicas e outros efeitos sonoros sem a necessidade de utilizar o XACT (a ferramenta de autoria de som que acompanha o Game Studio).

Assim, agora é possível tratar os efeitos sonoros da mesma forma que uma textura ou um modelo 3D, simplesmente carregando o arquivo através de Content.Load. Este novo sistema é interessante porque permite fazer várias coisas com as músicas, inclusive tocar arquivos da biblioteca do usuário. Os formatos suportados são mp3 e wma (para músicas) e wave (para efeitos sonoros).

Neste pequeno tutorial eu vou mostrar como carregar e tocar músicas e efeitos sonoros nos jogos.

Read more…

Diego Barboza Artigos, Programação, Tutoriais, XNA ,

Tutorial Blender – Usando e visualizando imagens de background

No último final de semana passei por um problema simples de resolver mas que é difícil de descobrir: como usar direito imagens de background. Pra quem não sabe, são imagens que auxiliam na modelagem de objetos no Blender. Veja um exemplo:

Uma das definições da imagem acima é o blueprint, que mostra o objeto de frente e de perfil. Quando alguém for fazer uma modelagem mais elaborada, o usuário vai fazer um esboço (que vai ser digitalizado ou escaneado caso seja à mão) de frente, perfil e algumas vezes de costas, pra ir modelando e seguindo a imagem de background.

O problema maior ao utilizar este método é fazer essa imagem aparecer na interface 3D do software.O objetivo deste tutorial é justamente ajudar os usuários que não conseguem ou não sabem como fazer isto. A versão utilizada é a 2.47.

Primeiro pegue um blueprint qualquer (pode ser de qualquer site. Estou usando o deste tutorial de character design no Blender), abra o Blender e vá em ViewUse Background Image:

Aí você aperte o botão Use Background Image pra abrir mais opções:

Escolha a imagem no seu sistema operacional e abra a mesma. Ela pode não aparecer logo de cara, mas já está configurada, como na imagem abaixo:

Aí que vem o macete mais importante. Se você reparar bem, no menu View existem várias opções: Side, Front, Top, Camera e User:

Ao escolher uma delas você vai ver a imagem de background. Por default a imagem acaba indo pra câmera, e ela tem de ir pra própria interface 3D:

Continua na próxima página.

Rodrigo Flausino Blender, Modelagem 3D, Tutoriais , ,

Tutorial: XNA Invasores – Parte 9

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.

    Diego Barboza Programação, Tutoriais, XNA , ,

    Tutorial: XNA Invasores – Parte 8

    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!

    Diego Barboza Programação, Tutoriais, XNA , ,

    Tutorial: XNA Invasores – Parte 2

    Logo do XNAAntes de começar a programação, geralmente um jogo passa por uma grande fase de planejamento e projeto. Não é difícil imaginar o motivo disto, afinal mesmo que um jogo casual não envolva centenas de profissionais nem custe milhões pra ser produzido, ele ainda é um software complexo e o rumo pode ser facilmente perdido se não houver um documento mostrando o rumo a ser seguido.

    Este tutorial não visa detalhar este planejamento, meu objetivo maior é ensinar a programar um jogo, porém pelo menos uma visão geral do que será feito deve ser estruturada para que a gente não se perca no processo.

    Obviamente, quando se faz um novo tipo de jogo a necessidade de análise (para estipular requisitos como jogabilidade, objetivos, personagens…) é bem maior, por isso mesmo eu decidi começar com um clone de um jogo bem conhecido. Uma vez que a maioria das pessoas conhece o Space Invaders, podemos apenas observar seu funcionamento e criar um modelo baseado no que vemos.
    Read more…

    Diego Barboza Programação, Tutoriais, XNA , ,