[Tens de ter uma conta e sessão iniciada para poderes visualizar esta imagem]

Melhorando a rebatida da bola

A rebatida da bola agora vai funcionar da seguinte forma. Toda vez que a bola colidir com o bastão, vamos pegar a tangente inversa da diferênça das variáveis x e y. Não se assustem galera, existe a função Math.Atan2(double y, double x) que calcular a tangente inversa de y / x, exceto que os sinais de ambos os argumentos são usados para determinar o quadrante do resultado. Esta função retorna o resultado em radianos, estando entre -PI e PI (inclusive). Veja a figura abaixo do que acontece com o vetor direção quando a bola colide com o bastão.

[Tens de ter uma conta e sessão iniciada para poderes visualizar esta imagem]

Esse ainda não é o melhor método para deixar uma rebatida legal, mas o resultado é bem interessante. Vamos então pegar o trecho de código que checava a colisão da bola com os bastões e fazer a seguinte alteração conforme o código abaixo.

Código:

// Checa a colisão da bola com as raquetes dos jogadores
// Jogador 1 (Raquete da esquerda)
if (ball.GetBounding().Intersects(bat1.GetBounding()))
{
    // Centro da bola
    Vector2 cBall = new Vector2(ball.GetBounding().Center.X, ball.GetBounding().Center.Y);
    cBall.Normalize();
    // Centro do bastão a esquerda (jogador 1)
    Vector2 cBat = new Vector2(bat1.GetBounding().Center.X, bat1.GetBounding().Center.Y);
    cBat.Normalize();
    // Angulo de direção
    double angDir = Math.Atan2(cBall.Y - cBat.Y, cBall.X - cBat.X);
    // Inverte a direção X da bola
    ball.Direction = new Vector2((float)Math.Cos(angDir), (float)Math.Sin(angDir));
    // Toca o efeito sonoro de colisão com a bola
    soundToc2.Play();
}
// Jogador 2 (Raquete da direita)
if (ball.GetBounding().Intersects(bat2.GetBounding()))
{
    // Centro da bola
    Vector2 cBall = new Vector2(ball.GetBounding().Center.X, ball.GetBounding().Center.Y);
    cBall.Normalize();
    // Centro do bastão a esquerda (jogador 2)
    Vector2 cBat = new Vector2(bat2.GetBounding().Center.X, bat2.GetBounding().Center.Y);
    cBat.Normalize();
    // Angulo de direção
    double angDir = Math.Atan2(cBall.Y - cBat.Y, cBall.X - cBat.X);
    // Inverte a direção X da bola
    ball.Direction = new Vector2((float)Math.Cos(angDir), (float)-Math.Sin(angDir));
    // Toca o efeito sonoro de colisão com a bola
    soundToc2.Play();
}


Definindo os estados do jogo

Vamos agora criar uma nova classe chamada PongState.cs, que definirá um enumerador contendo os estados do jogo. Kleber para por ai, não entendi o que é enumerador e nem sei o que são estes estados do jogo.


  • Estados do jogo: são os estados existentes de um determinado jogo que indicam como ele se encontra, por exemplo: tela de abertura, tela de jogo, tela de game over, pause, etc.

  • Enumerador: a grosso modo, é uma maneira elegante de dar nome para os números. Então podemos dizer que o número zero se chama tela inicial, o número 1 se chama tela de jogo e o número 3 game over.

Então após criar este arquivo, digite o seguinte código:

Código:


namespace TutorialPong
{
    public enum PameState
    {
        IntroScreen,
        SinglePlayer,
        MultiPlayer,
        GameOver,
    }
}


A palavra chave enum que utilizamos serve justamente para enumerar as palavras começando do 0, ou seja: ( 0 ) IntroScreen; ( 1 ) SinglePlayer; ( 2 ) MultiPlayer e ( 3 ) GameOver. Perceba que agora ao invés de perguntarmos, você esta na tela 1, podemos perguntar você é SinglePlayer, ou melhor, você esta no estado SinglePlayer do jogo. Viu como fica muito legível usando enum.

Alternando estre as telas

Agora na classe Game1.cs e precisaremos fazer algumas alterações, mas primeiro vamos criar 4 novas variáveis. Uma variável para armazenar o estado do jogo, outra para a imagem da tela inicial, outra para a imagem da tela final e por fim uma constante com a quantidade máxima de pontos do jogo.

Código:

// Variável que armazena o estado do jogo
// Iniciamos a variável na tela inicial (IntroScreen)
PongState state = PongState.IntroScreen;
// Variável para armazenar a imagem da tela inicial
Texture2D intro = null;
// Variável para armazenar a imagem da tela de game over
Texture2D gameover = null;
// Pontução máxima do jogo
const int POINT_COUNT = 15;


Agora no método LoadContent(), vamos carregar as novas telas antes de dar o play na música, digite então o seguinte código lá.

Código:

// Carrega a textura da tela inicial
intro = Content.Load<Texture2D>(@"Textures\intro");
// Carrega a textura de game over
gameover = Content.Load<Texture2D>(@"Textures\gameover");


Agora vai ser um pouco difícil ma vamos tentar. Após verificarmos se pressionamos a tecla ESC dentro do método Update(), lá no começo, digite o seguinte código:

Código:

switch(state)
{
  case PongState.InitroScreen:
      break;
  case PongState.SinglePlayer:
  case PongState.MultiPlayer:
      break;
  case PongState.GameOver:
      break;
}


Isso é um interruptor que vai verificar qual o estado do jogo, então caso um estado do jogo seja igual aquelas palavras que definimos, ele executara todo o código que estiver abaixo da palavra até o primeiro break.

Então entre o case PongState.IntroScreen e break, vamos colocar o seguinte código.

Código:

case PongState.IntroScreen:
    // Entradas do jogador
    if (keyState.IsKeyDown(Keys.D1))//Se esta pressionado tlecla 1
    {
        state = PongState.SinglePlayer;
        RestartGame();
    }
    if (keyState.IsKeyDown(Keys.D2))//Se esta pressionado tlecla 2
    {
        state = PongState.MultiPlayer;
        RestartGame();
    }
    break;


Se estivemos na tela inicial só precisamos então verificar qual a tecla que o usuário pressionou, se foi o Digito 1 (D1) ele vai para o estado de SinglePlayer, se foi o Digito 2 (D2) ele vai para o estado de MultiPlayer. Mas quem é RestarGame? RestarGame é um método que você deve digitar dentro da classe Game1.cs abaixo do método Draw().

Código:


public void RestartGame()
{
  // Inicializando o objeto bola
  ball.Position = new Vector2(393, 313);
  // Inicializando o objeto bastão 1
  bat1.Position = new Vector2(10, 290);
  // Inicializando o objeto bastão 2
  bat2.Position = new Vector2(775, 290);
  // Inicializando o score dos jogadores
  score[0] = 0; // Jogador 1
  score[1] = 0; // Jogador 2
}


Caso seja, game over o estado atual do jogo precisou verificar se for pressionado Enter e mudar o estado para a tela inicial novamente.

Código:

case PongState.GameOver:
    // Entradas do usuario
    if (keyState.IsKeyDown(Keys.Enter)) // Se pressionar ENTER
    {
        state = PongState.IntroScreen;
    }
    break;


Bom galera todo aquele código que estava no método Update(), vamos colocar e fazer algumas mudanças. Veja como fica:

Código:

case PongState.SinglePlayer:
case PongState.MultiPlayer:
    // Se pressionar a tecla ESC encerra o jogo
    if (keyState.IsKeyDown(Keys.Escape))
        Exit();
    // Entradas do jogador 1 (bastão do lado esquerdo)
    if (keyState.IsKeyDown(Keys.W))
        bat1.Direction = new Vector2(0.0f, -1.0f);
    else if (keyState.IsKeyDown(Keys.S))
        bat1.Direction = new Vector2(0.0f, 1.0f);
    else
        bat1.Direction = Vector2.Zero;
    // Atualiza a posição do bastão 1
    bat1.Update(gameTime);
 
    if (state == PongState.SinglePlayer)
    {
        // Se o jogo for para single player o computador
        // é atualizado automáticamente
        MoveBastaoComputador();
    }
    else
    {
        // Entradas do jogador 2 (bastão do lado direito)
        if (keyState.IsKeyDown(Keys.Up))
            bat2.Direction = new Vector2(0.0f, -1.0f);
        else if (keyState.IsKeyDown(Keys.Down))
            bat2.Direction = new Vector2(0.0f, 1.0f);
        else
            bat2.Direction = Vector2.Zero;
    }
    // Atualiza a posição do bastão 2
    bat2.Update(gameTime);
 
    // Checa colisões da bola com as paredes do campo
    // Verifica se a bola colidiu em baixo
    if (ball.Position.Y + ball.Texture.Height > 570.0f)
    {
        // Inverte a direção em Y do vetor
        ball.Direction *= new Vector2(1.0f, -1.0f);
        // Toca o efeito sonoro de colisão com o campo
        soundToc1.Play();
    }
    // Verifica se a bola colidiu em baixo
    if (ball.Position.Y < 70.0f)
    {
        // Inverte a direção em Y do vetor
        ball.Direction *= new Vector2(1.0f, -1.0f);
        // Toca o efeito sonoro de colisão com o campo
        soundToc1.Play();
    }
    // Atualiza a posiação da bola
    ball.Update(gameTime);
    // Verifica se alguem marcou ponto
    // Se a bola passar pela direita da tela
    if (ball.Position.X + ball.Texture.Width > 800.0f)
    {
        // Toca o efeito sonoro para marcar pontos
        soundPoint.Play();
        score[0] += 1; // aumenta um ponto ao score do jogador 1
        // Coloca a bola no centro do campo novamente
        ball.Position = new Vector2(386.0f, 310.0f);
        // Muda a direção de disparo da bola em X
        ball.Direction *= new Vector2(-1.0f, 1.0f);
    }
    // Se a bola passar pela esquerda da tela
    else if (ball.Position.X < 0.0f)
    {
        // Toca o efeito sonoro para marcar pontos
        soundPoint.Play();
        score[1] += 1; // aumenta um ponto ao score do jogador 2
        // Coloca a bola no centro do campo novamente
        ball.Position = new Vector2(386.0f, 310.0f);
        // Muda a direção de disparo da bola
        ball.Direction *= new Vector2(-1.0f, 1.0f);
    }
 
    // Checa a colisão da bola com as raquetes dos jogadores
    // Jogador 1 (Raquete da esquerda)
    if (ball.GetBounding().Intersects(bat1.GetBounding()))
    {
        // Centro da bola
        Vector2 cBall = new Vector2(ball.GetBounding().Center.X, ball.GetBounding().Center.Y);
        cBall.Normalize();
        // Centro do bastão a esquerda (jogador 1)
        Vector2 cBat = new Vector2(bat1.GetBounding().Center.X, bat1.GetBounding().Center.Y);
        cBat.Normalize();
        // Angulo de direção
        double angDir = Math.Atan2(cBall.Y - cBat.Y, cBall.X - cBat.X);
        // Inverte a direção X da bola
        ball.Direction = new Vector2((float)Math.Cos(angDir), (float)Math.Sin(angDir));
        // Toca o efeito sonoro de colisão com a bola
        soundToc2.Play();
    }
    // Jogador 2 (Raquete da direita)
    if (ball.GetBounding().Intersects(bat2.GetBounding()))
    {
        // Centro da bola
        Vector2 cBall = new Vector2(ball.GetBounding().Center.X, ball.GetBounding().Center.Y);
        cBall.Normalize();
        // Centro do bastão a esquerda (jogador 2)
        Vector2 cBat = new Vector2(bat2.GetBounding().Center.X, bat2.GetBounding().Center.Y);
        cBat.Normalize();
        // Angulo de direção
        double angDir = Math.Atan2(cBall.Y - cBat.Y, cBall.X - cBat.X);
        // Inverte a direção X da bola
        ball.Direction = new Vector2((float)Math.Cos(angDir), (float)-Math.Sin(angDir));
        // Toca o efeito sonoro de colisão com a bola
        soundToc2.Play();
    }
    // Verifica se algum jogador chegou ao limite de pontos da partida
    // Se o jogador 1 ganhou
    if (score[0] >= POINT_COUNT)
    {
        state = PongState.GameOver;
    }
    // Se o jogador 2 ganhou
    if (score[1] >= POINT_COUNT)
    {
        state = PongState.GameOver;
    }
    break;


Movimentos do computador (Modo Single Player)

Agora precisamos definir o método MoveBastaoComputador() que colocamos no estado SinglePlayer. Vamos fazer algo simples conforme o método abaixo, mas se quiser modificar fique a vontade, pois você vai precisar melhorar os desafios para ele ficar mais legal.

Código:

public void MoveBastaoComputador()
{
    // Bola indo para direita
    if (ball.Direction.X > 0.0f)
    {
        if (bat2.GetBounding().Center.Y < ball.GetBounding().Center.Y)
            bat2.Direction = new Vector2(0.0f, 1.0f);
        else if (bat2.GetBounding().Center.Y > ball.GetBounding().Center.Y)
            bat2.Direction = new Vector2(0.0f, -1.0f);
        else
            bat2.Direction = new Vector2(0.0f, 0.0f);
    }
    else
    {
        bat2.Direction = new Vector2(0.0f, 0.0f);
    }
}


No código acima, simplesmente verificamos se o centro do bastão está acima ou abaixo do centro da bola, e assim fazemos o bastão seguir naquela direção.

Desenhando as telas

Para finalizar, agora só falta desenhar as telas, e para desenhá-las vamos precisar fazer um switch igual fizemos no método Update, vamos ver como fica o método Draw. Veja como ficaria o método Draw.

Código:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    switch (state)
    {
        case PongState.IntroScreen:
            // Desenha a tela de apresentação (tela inicial do jogo)
            spriteBatch.Draw(intro, Vector2.Zero, Color.White);
            break;
 
        case PongState.SinglePlayer:
        case PongState.MultiPlayer:
            // Desenha a imagem de fundo
            spriteBatch.Draw(background, Vector2.Zero, Color.White);
            // Desenha a bola
            ball.Draw(spriteBatch);
            // Desenha o bastão do jogador 1
            bat1.Draw(spriteBatch);
            // Desenha o bastão do jogador 2
            bat2.Draw(spriteBatch);
            // Desenhando o score do jogador 1 no centro do quadrado
            Vector2 textSize = fontScore.MeasureString(score[0].ToString("000"));
            spriteBatch.DrawString(fontScore,
                score[0].ToString("000"),
                new Vector2(300, 35) - textSize / 2,
                Color.White);
            // Desenhando o score do jogador 2 no centro do quadrado
            textSize = fontScore.MeasureString(score[1].ToString("000"));
            spriteBatch.DrawString(fontScore,
                score[1].ToString("000"),
                new Vector2(500, 35) - textSize / 2,
                Color.White);
            break;
        case PongState.GameOver:
            // Desenha a tela de game over
            spriteBatch.Draw(gameover, Vector2.Zero, Color.White);
            break;
    }
    spriteBatch.End();
    base.Draw(gameTime);


Como podemos ver, desenhamos o que vamos precisamos em cada estado do jogo, lembrando que esta é uma forma de fazer isso de forma simples, mas existem outras formas melhores porém um pouco mais complexas para iniciar um aprendizado.

Resultado do tutorial

Caso não tenhamos esquecido de nada, podemos rodar nossa aplicação e ver o resultado conforme imagens abaixo:

[Tens de ter uma conta e sessão iniciada para poderes visualizar esta imagem]

[Tens de ter uma conta e sessão iniciada para poderes visualizar esta imagem]