Desenvolvimento

22 jul, 2014

Desenvolvendo jogos com MonoGame

Publicidade

Muitos desenvolvedores querem desenvolver jogos. E por que não? Jogos estão entre os mais vendidos na história da computação e as fortunas envolvidas no negócio de jogos continuam a atrair desenvolvedores. Como um desenvolvedor, eu com certeza gostaria de estar entre aqueles que desenvolveram o próximo Angry Birds ou Halo.

Na prática, o desenvolvimento de jogos é uma das áreas mais difíceis do desenvolvimento de software. Você deve se lembrar daquelas aulas de trigonometria, geometria e física que pensou que nunca usaria, e que passam a ser parte importante de um jogo. Além disso, seu jogo deve combinar som, vídeo e uma história de uma maneira que o usuário queira jogar mais e mais. E isso antes de escrever uma única linha de código!

Para facilitar as coisas, há diversos frameworks disponíveis para o desenvolvimento de jogos usando não somente C e C++, mas até C# ou JavaScript (sim, você pode desenvolver jogos tridimensionais para seu browser usando HTML5 e JavaScript).

Um desses frameworks é o Microsoft XNA, construído sobre a tecnologia Microsoft DirectX, que permite criar jogos para Xbox 360, Windows e Windows Phone. A Microsoft está descontinuando o XNA mas, enquanto isso, a comunidade open source introduziu um novo participante: MonoGame.

O que é o MonoGame?

MonoGame é uma implementação open source da API (Application Programming Interface) XNA. Ele implementa a API XNA não apenas para Windows, mas também para Mac OS X, Apple iOS, Google Android, Linux e Windows Phone. Isso significa que você pode desenvolver um jogo para qualquer uma dessas plataformas com apenas pequenas modificações. Isso é uma característica fantástica: você pode criar jogos usando C# que podem ser portados facilmente para todas as maiores plataformas desktop, tablet ou smartphone. É um grande empurrão para quem quer conquistar o mundo com seus jogos.

Instalando MonoGame no Windows

Você nem precisa ter o Windows para desenvolver com MonoGame. Você pode usar o MonoDevelop (uma IDE [Integrated Development Environment] open source para linguagens Microsoft .NET) ou Xamarin Studio, uma IDE cross-platform desenvolvida pela Xamarin. Com essas IDEs, você pode desenvolver usando C# no Linux ou Mac.

Se você é um desenvolvedor Microsoft .NET e usa o Microsoft Visual Studio diariamente, como eu, pode instalar o MonoGame no Visual Studio e usá-lo para criar seus jogos. Quando este artigo foi escrito, a última versão estável era a versão 3.2. Essa versão roda no Visual Studio 2012 e 2013 e permite que você crie um jogo desktop DirectX, que você vai precisar se quiser suportar toque no jogo.

A instalação do MonoGame traz diversos modelos para o Visual Studio, que você pode usar para criar seus jogos, como mostrado na Figura 1.

Figura 1. Novos modelos instalados pelo MonoGame
Figura 1. Novos modelos instalados pelo MonoGame

Para criar seu primeiro jogo, clique em MonoGame Windows Project e selecione um nome. O Visual Studio cria um novo projeto com todos os arquivos e referências necessárias. Se você executar esse projeto, obterá algo como mostrado na Figura 2.

Figura 2. Jogo criado com o modelo MonoGame
Figura 2. Jogo criado com o modelo MonoGame

Sem graça, não? Somente uma tela azul clara, mas esse é o início para qualquer jogo que você criar. Tecle Esc e a janela fecha.

Você pode começar a escrever seu jogo com o projeto que tem agora, mas existe um porém: você não poderá adicionar recursos, como imagens, sprites, sons ou fontes sem compilá-los em um formato compatível com o MonoGame. Para isso, você tem uma destas opções:

  • Instalar o XNA Game Studio 4.0.
  • Instalar o Windows Phone 8 software development kit (SDK).
  • Usar um programa externo, como o XNA content compiler.

XNA Game Studio

O XNA Game Studio tem tudo o que você precisa para criar jogos para Windows e Xbox 360. Ele também tem um compilador de conteúdo para compilar seus recursos para arquivos .xnb, que podem ser adicionados ao seu projeto MonoGame. Ele vem com a instalação apenas para o Visual Studio 2010. Se você não quer instalar o Visual Studio 2010 somente para isso, pode instalar o XNA Game Studio no Visual Studio 2012 (veja o link na seção “Para Mais Informações” deste artigo).

Windows Phone 8 SDK

Você não pode instalar o XNA Game Studio diretamente no Visual Studio 2012 ou 2013, mas o SDK Windows Phone 8 pode ser instalado sem problemas nessas duas IDEs. Você pode usá-lo para criar um projeto para compilar seus recursos.

XNA Content Compiler

Se você não quer instalar uma SDK para compilar seus recursos, você pode usar o XNA content compiler (veja o link em “Para Mais Informações”), um programa open source que pode compilar seus recursos para arquivos .xnb, que podem ser usados no MonoGame.

Criando seu primeiro jogo

O jogo anterior que foi criado com o modelo MonoGame é o ponto inicial para todos os jogos. Você irá usar o mesmo processo em todos os jogos. Em Program.cs, você tem a função Main. Essa função inicializa e executa o jogo:

static void Main()
{
    using (var game = new Game1())
        game.Run();
}

Game1.cs é o coração do jogo. Ali, você tem dois métodos que são chamados 60 vezes por segundo em um loop: Update e Draw. Em Update, você recalcula os dados para todos os elementos no jogo; em Draw, você desenha esses elementos. Note que esse é um loop muito estreito. Você tem 1/60 de segundo, ou seja, 16.7 milissegundos para calcular e desenhar os dados. Se você levar mais tempo que isso, o programa pode pular alguns ciclos Draw e você verá falhas de desenho em seu jogo.

Até recentemente, a entrada de dados para jogos em computadores desktop era o teclado e o mouse. A menos que o usuário tivesse comprado hardware extra, como volantes ou joysticks, você não poderia assumir que houvesse outros métodos de entrada. Com os novos equipamentos, como dispositivos Ultrabook™, Ultrabook 2 em 1, em PCs all-in-one, essas opções mudaram. Você pode usar entrada de toque e de sensores, dando aos usuários um jogo mais imersivo e realista.

Para este primeiro jogo, iremos criar um jogo de chutes de pênaltis de futebol. O usuário utilizará toque para “chutar” a bola, e o goleiro do computador tentará pegá-la. A direção e a velocidade da bola serão determinadas pelo toque do usuário. O goleiro irá escolher um canto e velocidade arbitrários para pegar a bola. Cada gol resulta em um ponto. Se não houver gol, o goleiro fica com o ponto.

Adicionando conteúdo ao jogo

O primeiro passo no jogo é adicionar conteúdo. Inicie adicionando o fundo do campo e a bola. Para fazer isso, crie dois arquivos .png: um para o campo de futebol (Figura 3) e outro para a bola (Figura 4).

Figura 3. O campo de futebol
Figura 3. O campo de futebol
Figura 4. A bola de futebol
Figura 4. A bola de futebol

Para usar esses arquivos no jogo, você deve compilá-los. Se você está usando o XNA Game Studio ou o SDK Windows Phone 8, deve criar um projeto de conteúdo XNA. Esse projeto não precisa estar na mesma solução, você irá usá-lo apenas para compilar os recursos. Adicione os recursos a esse projeto e compile-o. Em seguida, vá para o diretório destino e adicione os arquivos .xnb resultantes a seu projeto.

Eu prefiro usar o XNA Content Compiler, pois ele não requer um novo projeto e permite que você compile os recursos quando necessário. Abra o programa, adicione os arquivos à lista, selecione o diretório de saída e clique em Compile. Os arquivos .xnb estão prontos para ser adicionados ao projeto.

Content Files

Uma vez que os arquivos .xnb estão disponíveis, adicione-os à pasta Content de seu jogo. Você deve configurar a build action para cada arquivo como Content, e a opção Copy to Output Directory como Copy if Newer. Se não fizer isso, você terá um erro ao tentar carregar os recursos.

Crie dois campos para armazenar as texturas do campo e da bola:

private Texture2D _backgroundTexture;
private Texture2D _ballTexture;

Esses campos são carregados no método LoadContent:

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    _spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
    _ballTexture = Content.Load<Texture2D>("SoccerBall");
}

Note que os nomes das texturas são os mesmos que os nomes dos arquivos na pasta Content, mas sem a extensão.

Em seguida, desenhe as texturas no método Draw:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Green);

    // Set the position for the background    
    var screenWidth = Window.ClientBounds.Width;
    var screenHeight = Window.ClientBounds.Height;
    var rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
    // Draw the ball
    var initialBallPositionX = screenWidth / 2;
    var ínitialBallPositionY = (int)(screenHeight * 0.8);
    var ballDimension = (screenWidth > screenHeight) ? 
        (int)(screenWidth * 0.02) :
        (int)(screenHeight * 0.035);
    var ballRectangle = new Rectangle(initialBallPositionX, ínitialBallPositionY,
        ballDimension, ballDimension);
    _spriteBatch.Draw(_ballTexture, ballRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

Esse método limpa a tela com uma cor verde e desenha o fundo e a bola na marca do pênalti. O primeiro método spriteBatch Draw desenha o fundo redimensionado para o tamanho da janela, na posição 0,0; o segundo método desenha a bola na marca do pênalti, redimensionada proporcionalmente ao tamanho da janela. Não há movimento aqui, pois as posições não mudam. O próximo passo é movimentar a bola.

Movendo a bola

Para mover a bola, você deve recalcular sua posição para cada iteração do loop e desenhá-la na nova posição. Faça o cálculo da nova posição no método Update:

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();

    // TODO: Add your update logic here
    _ballPosition -= 3;
    _ballRectangle.Y = _ballPosition;
    base.Update(gameTime);

}

A posição da bola é atualizada em cada loop, subtraindo-se três pixels. Se você quiser fazer com que a bola se movimente mais rápido, você deve subtrair mais pixels. As variáveis _screenWidth, _screenHeight, _backgroundRectangle, _ballRectangle e _ballPosition são campos privados, inicializados no método ResetWindowSize:

private void ResetWindowSize()
{
    _screenWidth = Window.ClientBounds.Width;
    _screenHeight = Window.ClientBounds.Height;
    _backgroundRectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
    _initialBallPosition = new Vector2(_screenWidth / 2.0f, _screenHeight * 0.8f);
    var ballDimension = (_screenWidth > _screenHeight) ?
        (int)(_screenWidth * 0.02) :
        (int)(_screenHeight * 0.035);
    _ballPosition = (int)_initialBallPosition.Y;
    _ballRectangle = new Rectangle((int)_initialBallPosition.X, (int)_initialBallPosition.Y,
        ballDimension, ballDimension);
}

Esse método reinicializa todas as variáveis que dependem do tamanho da janela. Ele é chamado no método Initialize:

protected override void Initialize()
{
    // TODO: Add your initialization logic here
    ResetWindowSize();
    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
    base.Initialize();
}

Esse método é chamado em dois lugares diferentes: no início do processo e toda vez que o tamanho da janela muda. Initialize manipula ClientSizeChanged, de maneira que quando o tamanho da janela muda, as variáveis que dependem do tamanho da janela são reavaliadas e a bola é reposicionada para a marca do pênalti.

Se você executar o programa, você verá que a bola move-se em uma linha reta, mas não para quando o campo termina. Você pode reposicionar a bola quando ela alcança o gol com o seguinte código:

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();

    // TODO: Add your update logic here
    _ballPosition -= 3;
    if (_ballPosition < _goalLinePosition)
        _ballPosition = (int)_initialBallPosition.Y;

    _ballRectangle.Y = _ballPosition;
    base.Update(gameTime);

}

A variável _goalLinePosition é outro campo inicializado no método ResetWindowSize:

_goalLinePosition = _screenHeight * 0.05;

Você deve fazer outra mudança no método Draw: remover todo o código de cálculo de posições.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Green);

   var rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
    // Draw the ball
    
    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

O movimento é perpendicular ao gol. Se você quiser que a bola se movimente num ângulo, crie um campo _ballPositionX, incremente-o para movimentar para a direita ou decremente-o, para movimentar para a esquerda. Uma maneira melhor é usar um Vector2 para a posição da bola, como o seguinte:

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();

    // TODO: Add your update logic here
    _ballPosition.X -= 0.5f;
    _ballPosition.Y -= 3;
    if (_ballPosition.Y < _goalLinePosition)
        _ballPosition = new Vector2(_initialBallPosition.X,_initialBallPosition.Y);
    _ballRectangle.X = (int)_ballPosition.X;
    _ballRectangle.Y = (int)_ballPosition.Y;
    base.Update(gameTime);

}

Se você executar o programa, verá que a bola se move com um ângulo (Figura 5). O passo seguinte é fazer a bola se mover quando o usuário “chuta” ela.

Figura 5. Jogo com a bola em movimento
Figura 5. Jogo com a bola em movimento

Toque e gestos

Neste jogo, o movimento da bola deve iniciar com um “peteleco”. Esse toque determina a direção e a velocidade da bola.

No MonoGame, você pode ter entrada de toque usando a classe TouchScreen. Você pode usar os dados brutos de entrada ou usar a API de gestos. Os dados brutos trazem mais flexibilidade, pois você pode processar os dados de entrada da maneira que deseja, enquanto que a API de gestos transforma os dados brutos em gestos filtrados, de forma que você só recebe entrada para os gestos que deseja.

Embora a API de gestos seja mais fácil de usar, há alguns casos em que ela não pode ser usada. Por exemplo, se você quer detectar um gesto especial, como um X ou gestos com mais de dois dedos, deverá usar os dados brutos.

Para este jogo, precisamos apenas do peteleco, e a API de gestos suporta isso, então iremos usá-la. A primeira coisa a fazer é indicar quais os gestos que você quer, usando a classe TouchPanel. Por exemplo, o código:

TouchPanel.EnabledGestures = GestureType.Flick | GestureType.FreeDrag;

. . . faz com que o MonoGame detecte e notifique apenas quando for feito um peteleco ou arrastado o dedo. Então, no método Update, você pode processar os gestos como da seguinte maneira:

if (TouchPanel.IsGestureAvailable)
{
    // Read the next gesture    
    GestureSample gesture = TouchPanel.ReadGesture();
    if (gesture.GestureType == GestureType.Flick)
    {
        …
    }
}

Inicialmente, determine se um gesto está disponível. Se estiver, você pode chamar ReadGesture para obtê-lo e processá-lo.

Iniciando o movimento com toque

Habilite gestos de peteleco no jogo usando o método Initialize:

protected override void Initialize()
{
    // TODO: Add your initialization logic here
    ResetWindowSize();
    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
    TouchPanel.EnabledGestures = GestureType.Flick;
    base.Initialize();
}

Até agora, a bola estava em movimento enquanto o jogo estava em execução. Use um campo privado, _isBallMoving, para dizer ao jogo quando a bola está em movimento. No método Update, quando o programa detecta um peteleco, você deve configurar _isBallMoving para True, para iniciar o movimento. Quando a bola alcança a linha do gol, configure _isBallMoving para False e reposicione a bola:

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();

    // TODO: Add your update logic here
    if (!_isBallMoving && TouchPanel.IsGestureAvailable)
    {
        // Read the next gesture    
        GestureSample gesture = TouchPanel.ReadGesture();
        if (gesture.GestureType == GestureType.Flick)
        {
            _isBallMoving = true;
            _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
        }
    }
    if (_isBallMoving)
    {
        _ballPosition += _ballVelocity;
        // reached goal line
        if (_ballPosition.Y < _goalLinePosition)
        {
            _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
            _isBallMoving = false;
            while (TouchPanel.IsGestureAvailable)
                TouchPanel.ReadGesture();
        }
        _ballRectangle.X = (int) _ballPosition.X;
        _ballRectangle.Y = (int) _ballPosition.Y;
    }
    base.Update(gameTime);

}

A velocidade da bola não é mais fixa: o programa usa o campo _ballVelocity para configurar a velocidade da bola nas direções x e y. Gesture.Delta retorna a variação de movimento desde a última atualização. Para calcular a velocidade do peteleco, multiplique esse vetor pela propriedade TargetElapsedTime.

Se a bola está se movendo, o vetor _ballPosition é incrementado pela velocidade (em pixels por frame) até que a bola alcança a linha do gol. O código a seguir:

_isBallMoving = false;
while (TouchPanel.IsGestureAvailable)
    TouchPanel.ReadGesture();

. . . faz duas coisas: para a bola e remove todos os gestos da fila de entrada. Se você não fizer isso, o usuário poderá fazer gestos enquanto a bola se move, fazendo com que ela reinicie o movimento após ter parado.

Ao executar o jogo, você pode dar petelecos na bola e ela se moverá na direção do peteleco, com a velocidade do gesto. Entretanto, temos um porém: o código não detecta onde o gesto ocorreu. Você pode dar petelecos em qualquer ponto da tela (não somente na bola), e a bola iniciará o movimento. Você poderia usar gesture.Position para detectar a posição do gesto, mas essa propriedade sempre retorna 0,0 e assim ela não pode ser usada.

A solução é usar os dados brutos, obter a entrada de toque e ver se ela está próxima da bola. O código a seguir determina se a entrada de toque coincide com a bola. Se coincidir, configuramos o campo _isBallHit:

TouchCollection touches = TouchPanel.GetState();

if (touches.Count > 0 && touches[0].State == TouchLocationState.Pressed)
{
    var touchPoint = new Point((int)touches[0].Position.X, (int)touches[0].Position.Y);
    var hitRectangle = new Rectangle((int)_ballPositionX, (int)_ballPositionY, _ballTexture.Width,
        _ballTexture.Height);
    hitRectangle.Inflate(20,20);
    _isBallHit = hitRectangle.Contains(touchPoint);
}

Assim, o movimento só inicia se o campo _isBallHit é True:

if (TouchPanel.IsGestureAvailable && _isBallHit)

Se você executar o jogo, verá que o movimento da bola só começa se você der um peteleco nela. Ainda temos um problema aqui: se você atingir a bola muito devagar ou em uma direção que a bola não atinge a linha do gol, o jogo termina, pois a bola não volta nunca para a posição inicial. Você deve configurar um timeout para o movimento da bola. Quando o tempo expirar, o jogo reposiciona a bola.

O método Update tem um parâmetro: gameTime. Se você armazenar o valor de gameTime quando o movimento for iniciado, poderá saber o tempo em que a bola está em movimento e reinicializar o jogo quando esse tempo expirar:

if (gesture.GestureType == GestureType.Flick)
{
    _isBallMoving = true;
    _isBallHit = false;
    _startMovement = gameTime.TotalGameTime;
    _ballVelocity = gesture.Delta*(float) TargetElapsedTime.TotalSeconds/5.0f;
}

...

var timeInMovement = (gameTime.TotalGameTime - _startMovement).TotalSeconds;
// reached goal line or timeout
if (_ballPosition.Y <' _goalLinePosition || timeInMovement > 5.0)
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _isBallMoving = false;
    _isBallHit = false;
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

Adicionando um goleiro

O jogo está funcionando, mas ele precisa de um elemento de dificuldade: você deve adicionar um goleiro que fica se mexendo depois que a bola é chutada. O goleiro é um arquivo .png que é compilado pelo XNA Content Compiler (Figura 6). Você deve adicionar esse arquivo compilado à pasta Content, configurar a opção build action para Content, e configurar Copy to Output Directory para Copy if Newer.

Figura 6. O goleiro
Figura 6. O goleiro

O goleiro é carregado no método LoadContent:

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    _spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
    _ballTexture = Content.Load<Texture2D>("SoccerBall");
    _goalkeeperTexture = Content.Load<Texture2D>("Goalkeeper");
}

Você deve desenhá-lo no método Draw:

protected override void Draw(GameTime gameTime)
{

    GraphicsDevice.Clear(Color.Green);
   
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White);
    // Draw the ball
    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
    // Draw the goalkeeper
    _spriteBatch.Draw(_goalkeeperTexture, _goalkeeperRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

_goalkeeperRectangle contém o retângulo do goleiro na janela. Ele é mudado no método Update:

protected override void Update(GameTime gameTime)
{
    …

   _ballRectangle.X = (int) _ballPosition.X;
   _ballRectangle.Y = (int) _ballPosition.Y;
   _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
                    _goalKeeperWidth, _goalKeeperHeight);
   base.Update(gameTime);
}

Os campos _goalkeeperPositionY, _goalKeeperWidth e _goalKeeperHeight são atualizados no método ResetWindowSize:

private void ResetWindowSize()
{
    …
    _goalkeeperPositionY = (int) (_screenHeight*0.12);
    _goalKeeperWidth = (int)(_screenWidth * 0.05);
    _goalKeeperHeight = (int)(_screenWidth * 0.005);
}

A posição inicial do goleiro é no meio da tela, perto da linha do gol:

_goalkeeperPositionX = (_screenWidth - _goalKeeperWidth)/2;

O goleiro inicia o movimento ao mesmo tempo em que a bola. Ele se move de um lado para outro em um movimento harmônico. Esta senoide descreve seu movimento:

X = A * sin(at + δ)

A é a amplitude do movimento (a largura do gol), t o tempo do movimento, e a e δ são coeficientes aleatórios (isso fará com que o movimento seja aleatório, de modo que o usuário não possa prever a velocidade e o canto que o goleiro irá tomar).

Os coeficientes são calculados quando o usuário chuta a bola:

if (gesture.GestureType == GestureType.Flick)
{
    _isBallMoving = true;
    _isBallHit = false;
    _startMovement = gameTime.TotalGameTime;
    _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
    var rnd = new Random();
    _aCoef = rnd.NextDouble() * 0.005;
    _deltaCoef = rnd.NextDouble() * Math.PI / 2;
}

O coeficiente a é a velocidade do goleiro, um número entre 0 e 0.005 que representa uma velocidade entre 0 e 0.3 pixels/segundos (máximo de 0.005 pixels em 1/60 de segundo). O coeficiente δ é um número entre 0 e pi/2. Quando a bola está se movendo, você muda a posição do goleiro:

if (_isBallMoving)
{
    _ballPositionX += _ballVelocity.X;
    _ballPositionY += _ballVelocity.Y;
    _goalkeeperPositionX = (int)((_screenWidth * 0.11) *
                      Math.Sin(_aCoef * gameTime.TotalGameTime.TotalMilliseconds + 
                      _deltaCoef) + (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11);
    …
}

A amplitude do movimento é _screenWidth. A amplitude do movimento é (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11 ao resultado, de modo que o goleiro se movimente em frente ao gol. Agora é hora de fazer o goleiro pegar a bola.

Teste de colisão

Se você quiser saber se o goleiro pega a bola, deve saber se o retângulo da bola intercepta o retângulo do goleiro. Você faz isso no método Update, depois de calcular os dois retângulos:

_ballRectangle.X = (int)_ballPosition.X;
_ballRectangle.Y = (int)_ballPosition.Y;
_goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
    _goalKeeperWidth, _goalKeeperHeight);
if (_goalkeeperRectangle.Intersects(_ballRectangle))
{
    ResetGame();
}

é somente uma refatoração do código que reinicializa o jogo ao estado inicial:

private void ResetGame()
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
    _isBallMoving = false;
    _isBallHit = false;
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

Com esse código simples, o jogo sabe se o goleiro pegou a bola. Agora, você precisa saber se a bola atingiu o gol. Você faz isso quando a bola passa da linha do gol.

var isTimeout = timeInMovement > 5.0;
if (_ballPosition.Y < _goalLinePosition || isTimeout)
{
    bool isGoal = !isTimeout &&
        (_ballPosition.X > _screenWidth * 0.375) &&
        (_ballPosition.X < _screenWidth * 0.623);
    ResetGame();
}

A bola deve estar completamente dentro do gol, a sua posição deve estar entre o primeiro poste do gol (_screenWidth * 0.375) e o segundo poste (_screenWidth * 0.625 − _screenWidth * 0.02). Agora é hora de atualizar o placar do jogo.

Adicionando um placar

Para adicionar um placar ao jogo, você deve adicionar um novo recurso: um spritefont com o fonte usado no jogo. Um spritefont é um arquivo .xml descrevendo o fonte—a família do fonte, seu tamanho e peso, juntamente com outras propriedades. Em um jogo, você pode usar um spritefont como este:

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
  <Asset Type="Graphics:FontDescription">
    <FontName>Segoe UI</FontName>
    <Size>24</Size>
    <Spacing>0</Spacing>
    <UseKerning>false</UseKerning>
    <Style>Regular</Style>
    <CharacterRegions>
      <CharacterRegion>
        <Start> </Star>
        <End></End>
      </CharacterRegion>
    </CharacterRegions>
  </Asset>
</XnaContent>

Você deve compilar esse arquivo .xml com o XNA Content Compiler e adicionar o arquivo .xnb resultante à pasta Content do projeto; configure a opção build action para Content e Copy to Output Directory para Copy if Newer. O fonte é carregado no método LoadContent:

_soccerFont = Content.Load<SpriteFont>("SoccerFont");

Em ResetWindowSize, reinicialize a posição do placar:

var scoreSize = _soccerFont.MeasureString(_scoreText);
_scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);

Para manter o resultado do jogo, declare duas variáveis: _userScore e _computerScore. A variável _userScore é incrementada quando acontece um gol, e _computerScore é incrementado quando a bola vai para fora, o tempo expira ou o goleiro pega a bola:

if (_ballPosition.Y < _goalLinePosition || isTimeout)
{
    bool isGoal = !isTimeout &&
                  (_ballPosition.X > _screenWidth * 0.375) &&
                  (_ballPosition.X < _screenWidth * 0.623);
    if (isGoal)
        _userScore++;
    else
        _computerScore++;
    ResetGame();
}
…
if (_goalkeeperRectangle.Intersects(_ballRectangle))
{
    _computerScore++;
    ResetGame();
}

ResetGame recria e reposiciona o placar:

private void ResetGame()
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
    _isBallMoving = false;
    _isBallHit = false;
    _scoreText = string.Format("{0} x {1}", _userScore, _computerScore);
    var scoreSize = _soccerFont.MeasureString(_scoreText);
    _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

O método _soccerFont.MeasureString mede o string usando o fonte selecionado. Você vai usar essa medida para calcular a posição do placar. O placar será desenhado no método Draw:

protected override void Draw(GameTime gameTime)
{
…
    // Draw the score
    _spriteBatch.DrawString(_soccerFont, _scoreText, 
         new Vector2(_scorePosition, _screenHeight * 0.9f), Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

Ligando as luzes do estádio

Como um toque final, o jogo liga as luzes do estádio quando o nível de luz no ambiente está baixo. Os novos dispositivos Ultrabook e 2 em 1 têm, em geral, sensores de luz que você pode usar para determinar quanta luz há no ambiente e mudar a maneira como o fundo é desenhado.

Para aplicações desktop, você pode usar o Windows API Code Pack para o Microsoft .NET Framework, uma biblioteca que permite acessar recursos dos sistemas operacionais Windows 7 e mais novos. Para este jogo, iremos usar um outro caminho: as APIs de sensores WinRT. Embora elas tenham sido escritas para Windows 8, também estão disponíveis para aplicações desktop e podem ser usadas sem mudanças. Utilizando-as, você pode portar sua aplicação para Windows 8 Store sem mudar uma única linha de código.

O Intel® Developer Zone (IDZ) tem um artigo sobre como usar as APIs WinRT em uma aplicação desktop (veja a seção “Para Mais Informações”). Baseado nessa informação, você deve selecionar o projeto no Solution Explorer, dar um clique com o botão direito e selecionar Unload Project. Então, clique com o botão direito novamente e clique em Edit project. No primeiro PropertyGroup, adicione um rótulo TargetPlatFormVersion:

<PropertyGroup>
  <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
…
  <FileAlignment>512</FileAlignmen>
  <TargetPlatformVersion>8.0</TargetPlatformVersion>
</PropertyGroup>

Clique com o botão direito novamente e então em Reload Project. O Visual Studio recarrega o projeto. Quando você adicionar uma nova referência ao projeto, poderá ver a aba Windows no gerenciador de referências, como mostra a Figura 7.

Figura 7. A aba Windows no Gerenciador de Referências
Figura 7. A aba Windows no Gerenciador de Referências

Adicione a referência Windows ao projeto. Você também deve adicionar uma referência a System.Runtime.WindowsRuntime.dll. Se não puder encontrar esse assembly na lista, você pode navegar para a pasta .Net Assemblies. Na minha máquina, essa pasta está em C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.

Agora, você pode escrever código para detectar o sensor de luz:

LightSensor light = LightSensor.GetDefault();
if (light != null)
{

Se o sensor de luz estiver presente, o método GetDefault retorna uma variável não nula que você pode usar para detectar variações de luz. Você pode fazer isso manipulando o evento ReadingChanged, como a seguir:

LightSensor light = LightSensor.GetDefault();
if (light != null)
{
    light.ReportInterval = 0;
    light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10;
}

Se a leitura estiver abaixo de 10, a variável _lightsOn é True, e você pode usá-la para desenhar o fundo de outra maneira. Se você olhar o método Draw de spriteBatch,verá que o terceiro parâmetro é uma cor. Até agora, você usou apenas branco. Essa cor é usada para colorir o bitmap. Se usar branco, as cores do bitmap permanecem as mesmas; se usar preto, o bitmap será todo preto. Qualquer outra cor colore o bitmap. Você pode usar essa cor para ligar as luzes, uma cor verde quando as luzes estão desligadas e branco quando ligadas. No método Draw, mude o desenho do fundo:

_spriteBatch.Draw(_backgroundTexture, rectangle, _lightsOn ? Color.White : Color.Green);

Agora, quando você executa o programa, verá um fundo verde escuro quando as luzes estão desligadas e verde claro quando estão ligadas (Figura 8).

Figura 8. O jogo finalizado
Figura 8. O jogo finalizado

 

Agora você tem um jogo completo. Sem dúvida, ele não está acabado – necessita ainda DE muito polimento (animações quando acontece um gol, bola retornando quando o goleiro pega a bola ou atinge um poste) -, mas eu deixo isSo como lição de casa para você. O passo final é portar o jogo para Windows 8.

Portando o jogo para Windows 8

Portar um jogo MonoGame para outras plataformas é fácil. Você deve apenas criar um novo projeto na solução, do tipo MonoGame Windows Store Project, apagar o arquivo Game1.cs e adicionar os quatro arquivos .xnb da pasta Content da app Windows Desktop para a pasta Content do novo projeto. Você não vai adicionar novas cópias dos arquivos, mas sim links para os arquivos originais. No Solution Explorer, clique com o botão direito na pasta Content do novo projeto e em Add/Existing Files, selecione os quatro arquivos .xnb do projeto Desktop, clique na seta para baixo ao lado do botão Add e selecione Add as link. O Visual Studio adiciona os quatro links.

Em seguida, adicione o arquivo Game1.cs do velho projeto ao novo. Repita o procedimento que você fez com os arquivos .xnb: clique com o botão direito no projeto, clique em Add/Existing Files e selecione Game1.cs do outro projeto, clique na seta para baixo ao lado do botão Add e clique em Add as link. A última mudança a fazer está em Program.cs, onde você deve mudar o namespace para a classe Game1, porque você está usando a classe Game1 do projeto desktop.

Pronto – você criou um jogo para Windows 8!

Conclusão

Desenvolver jogos é uma tarefa difícil por si só. Você terá que lembrar suas aulas de geometria, trigonometria e física e aplicar todos aqueles conceitos ao desenvolvimento do jogo (não seria ótimo se os professores usassem jogos ao ensinar esses assuntos?).

MonoGame facilita um pouco essa tarefa. Você não precisa usar o DirectX, pode utilizar o C# para desenvolver seus jogos e você tem acesso completo ao hardware. Toque, som e sensores estão disponíveis para seus jogos. Além disso, você pode desenvolver um jogo e portá-lo, com pequenas alterações, para Windows 8, Windows Phone, Mac OS X, iOS ou Android. Esse é um bônus real quando você quer desenvolver jogos multiplataforma.

Para mais informações