O meu primeiro Jogo em MonoGame

Muitos programadores chegaram ao mundo da programação através do fascínio do desenvolvimento de jogos. Desde as cassetes de ZX Spectrum, que demoravam eternidades a carregar e a criação de jogos era uma tarefa muitas vezes hercúlea, até aos dias de hoje, a criação de jogos percorreu um longo caminho e hoje podemos encontrar várias plataformas dedicadas ao seu desenvolvimento.

Para facilitar a criação de jogos para múltiplas plataformas foi criada a framework MonoGame, baseada na framework XNA da Microsoft, que apresenta uma grande facilidade de aprendizagem. Seguindo o princípio “Escreve uma vez, corre em todo o lado”, ao desenvolvermos um jogo com MonoGame, ele irá correr em iOS, Android, Mac OS X, tvOS, Windows, Linux, Playstation4 e mais.

Neste artigo vamos criar um jogo do princípio ao fim, passo a passo, desde a criação do interface de utilizador até ao adicionar da lógica de jogo.

À semelhança do Jogo da Toupeira (Whack-a-Mole), o objectivo do jogo é tocar nos bonecos à medida que vão aparecendo, evitando que fujam com as revistas. Se um boneco ficar sem ser tocado por mais de cinco segundos, ele leva as revistas e o jogo acaba.

Ficheiro → Novo Jogo

Antes de dar início ao desenvolvimento do jogo, devemos verificar se temos o Xamarin instalado bem como a última build de desenvolvimento do MonoGame. Os passos que vamos utilizar aplicam-se tanto a Xamarin Studio como a MonoGame no Visual Studio.

Vamos primeiro criar um projecto partilhado onde iremos guardar toda a lógica de jogo, podendo este ser também partilhado com todas as plataformas alvo em que queremos correr o jogo. Dentro do Xamarin Studio, escolhemos File > New Solution > MonoGame > Library > MonoGame Shared Library, e damos nome ao projecto PapTap. Não nos podemos esquecer de adicionar projectos para todas as plataformas que queremos para o jogo.

Adicionamos um projecto MonoGame for Android Application e damos-lhe o nome PapTap.Android. Uma vez que o nosso jogo não irá tirar partido das configurações nativas do Android, como o NFC e outros componentes, podemos apagar a classe Game1 criada no projecto Android e adicionar uma referência ao projecto partilhado que criamos antes.

Para correr a aplicação no emulador seleccionado ou no Xamarin Android Player, pressionamos F5 ou Cmd+Enter. Deverá surgir um lindo ecrã azul.

Agora que configurámos a nossa solução devidamente, vamos começar a escrever o nosso jogo.

Anatomia de um Jogo

Para criar um jogo é necessário que vários componentes trabalhem em conjunto. A classe Game1 contém toda a lógica do jogo e é constituída por cinco métodos principais: Constructor, Initialize, LoadContent, Update, e Draw.

Cada um deles tem o objectivo de garantir que o jogo funciona devidamente, desde os efeitos sonoros à resposta ao input do utilizador e à execução da lógica do jogo.

Os métodos Constructor e Initialize são utilizados para desempenhar qualquer inicialização que o jogo necessite antes de começar a correr. O LoadContent tem a função de carregar qualquer conteúdo do jogo tal como texturas, sons, sombras, e outros componentes gráficos ou sonoros. O Update é utilizado para actualizar qualquer lógica de jogo enquanto o jogo é executado (recolher o input do utilizador ou actualizar o mundo), enquanto que o Draw deve ser utilizado exclusivamente para desenhar quaisquer gráficos que precisem ser exibidos.

Adicionar Items de Jogo

Para um jogo estar completo, necessita de texturas e efeitos sonoros. Estes são conhecidos no desenvolvimento de jogos como “content” ou “assets”. A maior parte das frameworks de jogos possui uma pipeline de conteúdo, a qual é utilizada apenas para pegar em items que estão em bruto e transformá-los num formato optimizado para o jogo. Na pasta Content, o ficheiro Content.mgcb é a pipeline de conteúdo do MonoGame. Tudo o que for adicionado a este ficheiro será optimizado e incluído no pacote final da aplicação.

Mais uma vez, todos os itens do jogo podem ser partilhados entre as plataformas alvo. Assim, arrastamos o Content Directory do projecto Android para o Projeto Partilhado. Devemos certificar-nos que a Build Action do ficheiro Content.mcgb está definida para MonoGameContentReference. Se a Build Action não aparecer nas opções clicamos com o botão direito do rato no ficheiro Content.mcgb, selecionamos Propriedades e inserimos manualmente o texto toMonoGameContentReference. Em seguida descarregamos os itens PapTap e fazemos a extração para a pasta Content do Projeto Partilhado.

No MonoGame encontramos um Editor de Pipeline especial que facilita imenso o trabalho com os itens do jogo.

Fazemos duplo clique no ficheiro Content.mcgb para abrir o editor de Pipeline e adicionamos os ficheiros de items que acabámos de descarregar.

MonoGame: pipeline

Agora que o conteúdo do jogo está optimizado para uso na nossa aplicação, podemos usar os itens no nosso jogo.

Criando o Interface de Utilizador do PapTap

Para carregar os itens de jogo recorremos a um ContentManager que é exposto por default através da propriedade Content da classe Game. Para começar vamos importar alguns namespaces necessários e declarar campos para armazenar os itens de jogo.

using System.Collections.Generic;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

public class Game1 : Game {
  GraphicsDeviceManager graphics;
  SpriteBatch spriteBatch;
  Texture2D monkey;
  Texture2D background;
  Texture2D logo;
  SpriteFont font;
  SoundEffect hit;
  Song title;
}

De seguida é necessário carregar os itens. Para isso adicionamos o seguinte código ao método LoadContent, pois é onde todos os itens devem ser carregados em MonoGame.

monkey = Content.Load ("monkey");
background = Content.Load("background");
logo = Content.Load("logo");
font = Content.Load("font");
hit = Content.Load("hit");
title = Content.Load("title");
MediaPlayer.IsRepeating = true;
MediaPlayer.Play(title);

Agora que carregámos o nosso conteúdo, desde as texturas ao áudio, é hora de desenhar a interface do utilizador no ecrã. Para desenhar imagens 2D e texto usamos a classe SpriteBatch. Para fazer o rendering de forma eficiente, o desenho é compactado junto e as sprites devem ser desenhadas entre os métodos da SpriteBatch, Begin e End. De seguida atualizamos o método Draw para desenhar a textura do boneco no ecrã, utilizando o campo SpriteBatch que acabámos de criar.

protected override void Draw(GameTime gameTime) {
  graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
  spriteBatch.Begin();
  spriteBatch.Draw(monkey, Vector2.Zero);
  spriteBatch.End();
  base.Draw(gameTime);
}

Para correr a aplicação carregamos Ctrl+Enter ou na tecla F5. Deveremos ver o boneco com a revista e ouvir a música que definimos no LoadContent. Agora que já temos os itens de jogo a carregar, vamos construir o restante do interface do utilizador para jogar PapTap.

Construindo o Interface do Utilizador do PapTap

Habitualmente nos jogos tradicionais do estilo Whack-a-Mole aparecem toupeiras aleatoriamente no ecrã e devem ser clicadas para que desapareçam. Em vez de fazermos o rendering aleatório de bonecos no ecrã, podemos utilizar uma grelha para ajudar e garantir que os bonecos não se sobrepõem de forma a que a experiência do utilizador seja consistente. A grelha é constituída por várias células. Cada célula contém um rectângulo, cor, temporizador decrescente, e valor de transição que será utilizado para fazer o fade in do boneco. Vamos então copiar e colar para o ficheiro Game1.cs a seguinte classe GridCell.

public class GridCell {
  public Rectangle DisplayRectangle;
  public Color Color;
  public TimeSpan CountDown;
  public float Transition;

  public GridCell() {
    Reset();
  }

  public bool Update(GameTime gameTime) {
    if (Color == Color.White) {
      Transition += (float) gameTime.ElapsedGameTime.TotalMilliseconds / 100f;
      CountDown -= gameTime.ElapsedGameTime;
      if (CountDown.TotalMilliseconds <= 0) {
        return true;
      }
    }
    return false;
  }

  public void Reset() {
    Color = Color.TransparentBlack;
    CountDown = TimeSpan.FromSeconds(5);
    Transition = 0f;
  }

  public void Show() {
    Color = Color.White;
    CountDown = TimeSpan.FromSeconds(5);
  }
}

Para reiniciar a célula ao seu estado default onde o boneco está escondido utilizamos o método Reset, o qual é chamado pelo utilizador ao clicar num boneco. Este método é utilizado para definir o temporizador para cinco segundos quando o boneco aparece no ecrã. O método Update é chamado em cada frame para atualizar o temporizador decrescente, ajudando a perceber se o utilizador não clicou no boneco dentro da janela de tempo dos cinco segundos. Uma vez definidas as células, vamos definir a grelha. Começamos por criar um novo campo List chamado grelha (grid).

List grid = new List();

Para calcular os rectângulos de exibição para cada célula, adicionamos o seguinte código ao método LoadContent.

var viewport = graphics.GraphicsDevice.Viewport;
var padding = (viewport.Width / 100);
var gridWidth = (viewport.Width - (padding * 5)) / 4; var gridHeight = gridWidth;

for (int y = padding; y < gridHeight*5; y += gridHeight+padding) {
  for (int x = padding; x < viewport.Width-gridWidth; x += gridWidth+padding) {
    grid.Add (new GridCell() {
        DisplayRectangle = new Rectangle(x, y, gridWidth, gridHeight)
    });
  }
}

Se quisermos que o jogo pareça bem em todos os fatores de forma, devemos ter em conta o tamanho do ecrã em vez dos valores posicionais definidos no código. O GraphicsDevice.ViewPort proporciona-nos uma forma dinâmica de trabalhar com diferentes factores de forma. No código acima, adicionámos 10% da largura do ecrã como espaçamento entre as células e calculámos uma largura e altura para a grelha utilizando a mesma propriedade. Podemos fazer loop através das coordenadas (X,Y) para cada linha e coluna e calcular o rectângulo de exibição.

Substituímos o método Draw com o código para desenhar os nossos bonecos.

protected override void Draw(GameTime gameTime) {
  graphics.GraphicsDevice.Clear(Color.SaddleBrown);
  spriteBatch.Begin();
  foreach (var square in grid)
    spriteBatch.Draw(monkey, destinationRectangle: square.DisplayRectangle, color: Color.White);
  spriteBatch.End();
  base.Draw(gameTime);
}

Por fim, queremos que o nosso jogo corra apenas em retrato, por isso adicionamos o seguinte código ao construtor.

graphics.SupportedOrientations = DisplayOrientation.Portrait;

Corremos a aplicação e deveremos ver uma grelha cheia de bonecos!

MonoGame: grelha inicial do PapTap

Posto isto, o interface de utilizador para o nosso jogo construído com MonoGame está completo.

Publicado na edição 53 (PDF) da Revista PROGRAMAR.