Jogo gráfico

Pong

O objetivo deste projeto é construir um jogo monousuário com interface gráfica 2D. O aluno deve escolher um destes jogos 1):

Clássicos:

Recentes:

Você pode propor outros jogos ao professor para análise.

Requisitos

Requisitos do jogo:

Requisitos do código:

Critérios de avaliação

A biblioteca Allegro

A biblioteca gráfica Allegro permite a manipulação de gráficos simples e áudio, sendo bem adaptada para a construção de jogos 2D. É uma biblioteca mais simples e limitada que a famosa biblioteca SDL, sendo mais adequada para iniciantes e para projetos menores.

Algumas de suas características:

Instalação da biblioteca Allegro 5 em Linux (Ubuntu, Mint, Debian):

sudo apt-get install liballegro5-dev

Uso:

Bibliotecas gráficas como a Allegro e SDL operam usando um esquema interno de buffer duplo, ou seja, dois buffers gráficos distintos que são usados em conjunto:

Esses buffers devem ser periodicamente alternados pelo programador (pseudocódigo):

fill_color (black) ;  // operações feitas no buffer 1, não são visíveis
draw (...) ;          // na tela neste momento.
draw (...) ;
...
flip_buffers () ;     // troca buffer 1 com 2, mostrando o novo conteúdo

As técnicas des buffer duplo e page flipping são implementadas nativamente na biblioteca Allegro, fornecendo imagens mais estáveis e melhor desempenho gráfico.

Estrutura básica de um jogo

A maioria dos jogos segue uma estrutura de código similar. O funcionamento do jogo segue uma “máquina de estados” que define o momento atual do jogo e um laço principal que implementa o jogo em si. Ambos são explicados brevemente nesta seção.

Estado das entidades

Cada entidade do jogo (jogador, bola, etc) tem um conjunto de atributos que a representam, que normalmente é implementado em um struct. Por exemplo, no jogo Pong a bola poderia ser representada pela seguinte estrutura:

// estrutura de uma bola
typedef struct
{
  short x, y ;     // posição atual
  short sx, sy ;   // velocidade atual
  short w, h ;     // dimensões
  color_t color ;  // cor atual
}
ball_t ;
 
// cria uma bola
ball_t ball ;

Estruturas como essa devem ser construídas para cada entidade ativa do jogo.

Máquina de estados

O funcionamento do jogo é gerenciado através de uma máquina de estados. Cada estado do jogo define o que deve ser feito naquele momento, as transições possíveis entre estados e os eventos que levam de um estado a outro. Por exemplo, a máquina de estados do jogo Pong é composta dos seguintes estados:

O relacionamento entre esses estados pode ser representado graficamente:

Essa máquina de estados pode ser facilmente implementada em C:

// estados possíveis do jogo Pong
enum {INICIO, SERVINDO, JOGANDO, FIMPART, FIMJOGO} state ;
 
// programa principal do jogo
int main ()
{
  state = INICIO ;
  for (;;)
    switch (state)
    {
      case INICIO  : state_init () ;  break ;
      case SERVINDO: state_serve () ; break ;
      case JOGANDO : state_play () ;  break ;
      case FIMPART : state_over () ;  break ;
      case FIMJOGO : state_close () ; break ;
      default: break ;
    }
}

Dessa forma, as funções state_init() e demais são usadas para implementar o comportamento de cada etapa do jogo. A variável state pode ser modificada dentro dessas funções para trocar de estado conforme necessário.

O laço principal

Durante o jogo propriamente dito, o programa deve continuamente ler as entradas de dados (teclado, mouse, …), atualizar o estado dos jogadores e demais entidades do jogo de acordo com essas entradas e desenhar a tela. Esse comportamento é chamado o laço principal do jogo.

O pseudocódigo a seguir dá um exemplo do laço principal do jogo Pong (simplificado):

// define estado inicial das entidades 
inicia_jogadores ()
inicia_bola ()

// laço principal
faça
  ler_entrada (teclado, mouse, ...)
  atualiza_estado_jogadores ()
  atualiza_estado_bola ()
  desenha_tela ()
até fim da partida

Esse ciclo deve ser repetido rapidamente (dezenas de vezes por segundo) para dar a impressão de fluidez do jogo.

O tutorial de jogo da biblioteca Allegro explica com detalhes o funcionamento do laço principal.

Controle da taxa de quadros

A tela do jogo deve ser atualizada com velocidade suficiente para dar fluidez ao jogo. De preferência, a taxa de atualização da tela (taxa de quadros por segundo ou frame rate) deve ser constante, para que a animação seja consistente.

Em um jogo simples, a quantidade de cálculo a efetuar antes de cada atualização de estado e de tela é geralmente pequena, se comparada à capacidade de processamento do computador. Por isso, é interessante pausar a execução por alguns milissegundos a cada ciclo, para garantir uma taxa de quadros constante (e também diminuir o consumo de CPU).

Uma forma de controlar a taxa de quadros é através de pausas (pseudocódigo):

// taxa de quadros por segundo
frame_rate = 60

// duração de cada quadro, em ms
t_quadro = 1000 / frame_rate

// laço principal do jogo
repita

   t_inicio = relogio_ms ()

  // processamento do jogo
  ...

  // espera um tempo para completar o quadro
  t_final = relogio_ms ()
  t_pausa = t_quadro - (t_final - t_inicio)
  pausa_ms (t_pausa)

até o fim da partida

Outra forma, que é usada frequentemente na Allegro 5, é a programação por eventos. Neste caso específico, programa-se um temporizador (timer) para gerar um evento a cada quadro; quando o evento ocorre, o processamento do jogo é feito:

// taxa de quadros por segundo
frame_rate = 60

// duração de cada quadro, em ms
t_quadro = 1000 / frame_rate

// define timer (um evento a cada X ms)
define_timer (t_quadro)

// laço principal do jogo
repita

   // espera o próximo evento do timer
   espera_evento (timer)

  // processamento do jogo
  ...

até o fim da partida

Desenvolvimento de jogos:

Sprites e sons:

1)
Diversos outros jogos foram desconsiderados porque existem muitas implementações prontas usando C e Allegro.
2)
Não é a mais poderosa, mas tem um bom compromisso entre funcionalidades e facilidade de uso.