Criando jogos em Delphi – Parte II

25 06 2008

- Imagens -

    No primeiro tutorial eu expliquei um pouco sobre como usar as funções da GDI para desenvolver um pequeno "jogo". Agora vou explicar como usar imagens nos seus jogos. Desenhar imagens com a GDI do windows é muito mais rápido do que usar as funções de desenho, já que não é preciso nenhum cálculo, basta copiar os pixels para a tela.

    Vamos usar para este exemplo imagens no formato bitmap padrão do windows. Exceto pelo fato de não serem comprimidas, elas são a melhor opção para jogos usando a GDI (imagens no formato JPEG podem ser usadas para fundos, mas nunca para sprites).

» Baixe aqui o arquivo com as imagens

- Considerações sobre resolução e número de cores -

    Desenhar bitmaps de 640 x 480 pixels com 256 cores é mais rápido do que desenhar um bitmap de 800 x 600 com o mesmo número de cores. Assim como desenhar um bitmap de 640 x 480 pixels com 256 cores é mais rápido do que desenhar um bitmap do mesmo tamanho com 24 ou 32 bits de cores. Além disso, o número de cores do backbuffer (o bitmap em que desenhamos antes de enviar para a tela) tem que ter o mesmo número de cores que os bitmaps que serão desenhados nele (do contrário será preciso converter os bitmaps a se desenhar antes, o que pode demorar um pouco). Um backbuffer de 16 bits é uma boa escolha (mesmo para bitmaps de 256 cores, que tem 8 bits). Jogos para computadores antigos precisam ter uma resolução e um número de cores menor (no máximo 256). A resolução do computador atual também afeta o desempenho do jogo. Quanto maior, mais coisas para desenhar, portanto um jogo em fullscreen será mais rápido do que um jogo rodado em janela.

- Carregando as imagens -

    Antes de usar as imagens precisamos carregá-las para a memória. O motivo? O acesso à memória é muuuito mais rápido do que o acesso ao disco. Mesmo assim temos que tomar cuidado para não carregar coisas demais para a memória. Como esse será um jogo bem simples e vamos usar apenas algumas imagens iremos carregá-las todas de uma vez para a memória, mas em um jogo maior é melhor carregar as imagens correspondentes à cada parte apenas quando for necessário (ex.: ao iniciar uma fase, ao começar um novo mapa, etc.)

    Antes de encher a página de códigos (nem são tantos assim) vou explicar um pouco sobre as diferentes maneiras de carregar e gerenciar imagens na memória.

- Utilizando TImages -

    A forma mais simples de carregar imagens para a memória no Delphi (mas não tão boa ou flexível) é usar TImages. Quando você adiciona um TImage ao formulário e coloca uma imagem nele o Delphi salva essa imagem em um arquivo de recurso (.RES) e carrega a imagem para a memória assim que o formulário é criado. Esse arquivo .RES é embutido no final do arquivo .EXE, então tudo o que você precisa fazer é distribuir um único .EXE e usar a propriedade Graphic do TPicture para desenhar a imagem. A vantagem desse método é que você não precisa se preocupar em carregar ou liberar a imagem da memória, o Delphi fará isso para você, além disso fica mais difícil um usuário comum copiar as imagens do seu jogo (embora tenha vários editores de resources por aí). A desvantagem é não poder usar esta técnica para muitas imagens ou imagens muito pesadas.


Usando TImages para carregar as imagens.

- Carregando imagens do disco -

    Essa é a técnica mais utilizada pelos jogos (e a que vamos usar neste tutorial). Consiste em manter algumas variáveis e carregar as imagens do disco para estas variáveis quando for preciso utilizá-las. Aqui vamos usar uma matriz de bitmaps para armazenar todas as imagens na memória (não se esqueça de deixar todas as imagens na mesma pasta do programa).

    Além dessas duas técnicas existem outras, que utilizam arquivos de recursos dinâmicos, dlls ou mesmo um grande arquivo com todas as imagens nele, mas eu não vou falar deles aqui.

- Um pequeno exemplo -

    Vamos construir um pequeno joguinho de pong bem simples. Crie um formulário e deixe a ClientWidth em 640 e a ClientHeight em 480 (a ClientWidth e ClientHeight se referem apenas à área útil do formulário, sem contar as bordas ou a barra de título), essa será a resolução do nosso joguinho. Declare as seguintes variáveis globais:

var
  Form1: TForm1;
  //backbuffer
  bbuffer: TBitmap;
  //matriz de bitmaps do jogo
  bitmaps: array of TBitmap;

    Declaramos uma matriz de bitmaps para carregar todas as imagens do jogo para a memória, agora precisamos carregar estes gráficos. Lembre-se que temos que inicializar os bitmaps antes de carregar as imagens e liberá-los da memória quando não precisarmos mais deles.

//função que carrega uma nova imagem na matriz
procedure loadImage(imagem: string);
begin
  //aloca um novo espaço na memória para a imagem
  //como aqui estamos usando matrizes dinâmicas
  //fica bem fácil fazer isso.
  SetLength(bitmaps, Length(bitmaps) + 1);
  //cria e carrega a imagem
  bitmaps[high(bitmaps)]:= TBitmap.Create;
  with bitmaps[high(bitmaps)] do
  begin
    //carrega do arquivo no disco para a memória
    LoadFromFile(imagem);
    //adiciona a transparência
    Transparent:= true;
    //modifica a cor de transparência para magenta
    TransparentColor:= clFuchsia;
  end;
end;
 
//função que libera todos os bitmaps da matriz
procedure freeBitmaps;
var i: Integer; //variável de controle do laço
begin
  //percorre toda a matriz...
  for i:= to High(bitmaps) do
  begin
    //libera o bitmap da memória..
    FreeAndNil(bitmaps[i]);
  end;
  //libera o espaço alocado para a matriz
  bitmaps:= nil;
end;
 
procedure LoadGameGraphics;
begin
  //carrega todas as imagens do jogo
  LoadImage('0.bmp');
  LoadImage('1.bmp');
  LoadImage('2.bmp');
  LoadImage('3.bmp');
  LoadImage('4.bmp');
  LoadImage('5.bmp');
  LoadImage('6.bmp');
  LoadImage('7.bmp');
  LoadImage('8.bmp');
  LoadImage('9.bmp');
  LoadImage('ball.bmp');
  LoadImage('bar.bmp');
  LoadImage('cpu.bmp');
  LoadImage('cts_text.bmp');
  LoadImage('field.bmp');
  LoadImage('player.bmp');
end;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
  //quando o form for criado carregamos as imagens
  LoadGameGraphics;
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
  //quando o form for destruído, liberamos as imagens
  freeBitmaps;
end;

    Acho que vocês viram como a coisa funciona. Temos uma função que carrega as imagens na matriz, e chamamos ela quando precisamos carregar alguma imagem, no caso do nosso joguinho, assim que o formulário for criado.

    Vamos colocar agora algumas variáveis para controlar os objetos do nosso jogo.

type
  //objeto com as informações sobre a bolinha
  TBall = record
    //posição no plano do jogo
    X, Y: Single;
    //largura e altura da bolinha
    W, H: Integer;
    //velocidade horizontal e vertical (determina a
    //direção (ângulo) da bolinha
    Vx, Vy: Single;
    //velocidade da bolinha
    Speed: Single;
  end;
 
  //objeto com as informações da barrinha da AI
  TCPUBar = record
    //posição da barrinha no plano do jogo
    X, Y: Single;
    //largura e altura
    W, H: Integer;
    //velocidade máxima da barrinha
    Speed: Single;
  end;
 
  //objeto com as informações da barrinha do jogador
  TPlayerBar = record
    //posição da barrinha no plano do jogo
    X, Y: Single;
    //largura e altura
    W, H: Integer;
  end;
 
//precisamos criar algumas variáveis para manipular
//esses tipos e outras coisas no jogo
 
var
  Form1: TForm1;
 
  ball: TBall;
  bar_cp: TCPUBar;
  bar_p1: TPlayerBar;  
 
  //indica se o jogo está pausado ou não
  paused: Boolean = true;
  //pontos do jogador
  score_p1: Integer = 0;
  //pontos da AI
  score_cp: Integer = 0;
 
  //buffer de desenho do jogo
  bbuffer: TBitmap;
  //bitmaps do jogo
  bitmaps: array [0..15of TBitmap;

    Adicione um TTimer no formulário, assim como fizemos da última vez. É esse Timer que vai controlar o loop principal do jogo até ele terminar. O nosso jogo irá terminar apenas quando o usuário fechar a janela. No evento onTimer precisamos fazer algumas coisas:

  • Analisar as informações do jogo e mover os objetos

  • Verificar as entradas do usuário e processá-las no jogo (mover
    a barrinha)

  • Verificar colisões com a barrinha ou o cenário

  • Desenhar os objetos no backbuffer

  • Desenhar o backbuffer na tela

    Poderíamos colocar todas essas funções dentro do evento OnTimer, mas ao invés disso vamos usar algumas pequenas funções para cada uma dessas funcionalidades e chamá-las dentro do evento OnTimer. Assim o código fica mais organizado e menos repetitivo. Veja abaixo o código do evento OnTimer.

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  //se o jogo não estiver parado
  if not paused then
  begin
    //verifica colisões da bolinha
    ball_check_collision;
    //move a bolinha
    ball_move;
    //controla a AI da barrinha do computador
    bar_cp_handle;
  end;
 
  //limpa a tela anterior para desenharmos a nova tela
  bbuffer.Canvas.FillRect(Rect(0,0,640,480));
 
  //desenha o fundo do jogo
  bbuffer.Canvas.Draw(3777, bitmaps[14]);
 
  //pontuação do jogador (texto)
  bbuffer.Canvas.Draw(3710, bitmaps[15]);
  //chama a função que cria a imagem da pontuação
  CreateScore(19117, score_p1);
  //pontuação da AI (texto)
  bbuffer.Canvas.Draw(42410, bitmaps[12]);
  //chama a função que cria a imagem da pontuação
  CreateScore(50717, score_cp);
 
  //Desenha a barrinha do jogador
  bbuffer.Canvas.Draw(Round(bar_p1.x), Round(bar_p1.y), bitmaps[11]);
  //Desenha a barrinha da AI
  bbuffer.Canvas.Draw(Round(bar_cp.x), Round(bar_cp.y), bitmaps[11]);
  //Desenha a bolinha
  bbuffer.Canvas.Draw(Round(ball.x), Round(ball.y), bitmaps[10]);
 
  //temos uma imagem para indicar que o jogo está
  //parado, então, caso esteja desenhamos a imagem
  if paused then
    bbuffer.Canvas.Draw(156240, bitmaps[13]);
 
  //finalmente envia tudo isso para a tela
  Canvas.Draw(0,0,bbuffer);
end;

Função que checa se a bolinha colidiu com alguma coisa

//Função que verifica as colisões da bolinha
procedure ball_check_collision;
var bt: Single;
begin
  //distância da bolinha até o topo do cenário
  bt:= 77 + (ball.H / 2);
  //verifica se a bolinha colidiu com o topo do cenário
  if ball.Y < bt then
  begin
    //reflete a bolinha com o mesmo ângulo
    ball.Vy:= -ball.Vy;
    //move um pouco a bolinha para evitar outras colisões
    ball.Y:= bt + (bt - ball.Y);
  end;
 
  //distância da bolinha até o fundo do cenário
  bt:= 77 366 - ball.H - 6;
  //verifica se a bolinha colidiu com o fundo do cenário
  if ball.y > bt then
  begin
    //reflete a bolinha com o mesmo ângulo
    ball.Vy:= -ball.Vy;
    //move um pouco a bolinha para evitar outras colisões
    ball.y:= bt - (ball.y - bt);
  end;
 
  //distância da bolinha até a barrinha do jogador
  bt:= bar_p1.X + bar_p1.w - (ball.W / 2);
  //verifica se a bolinha colidiu com a barrinha do jogador
  if (ball.x < bt) and ((ball.y >= bar_p1.Y) and (ball.y + ball.h < bar_p1.Y + bar_p1.h)) then
  begin
    //reflete a bolinha com o mesmo ângulo
    ball.Vx:= -ball.Vx;
    //aumenta a velocidade da bolinha
    ball.Speed:= ball.Speed + 0.5;
    //move um pouco a bolinha para evitar colisões
    ball.X:= bt + (bt - ball.x);
  end;
 
  //distância da bolinha até a barrinha da AI
  bt:= bar_cp.X - ball.w + (ball.w / 2);
  //verifica se a bolinha colidiu com a barrinha da AI
  if (ball.x > bt) and ((ball.y >= bar_cp.y) and (ball.y + ball.h < bar_cp.Y + bar_cp.h)) then
  begin
    //reflete a bolinha com o mesmo ângulo
    ball.Vx:= -ball.Vx;
    //aumenta a velocidade da bolinha
    ball.Speed:= ball.Speed + 0.5;
    //move um pouco a bolinha para evitar colisões
    ball.x:= bt - (ball.x - bt);
  end;
 
  //verifica se a bolinha saiu à esquerda
  if ball.x < 37 then
  begin
    //ponto da AI
    Inc(score_cp);
    //reinicia o jogo
    InitGame;
  end;
  //verifica se a bolinha saiu à direita
  if ball.X + ball.w > 37 566 then
  begin
    //ponto do jogador
    Inc(score_p1);
    //reinicia o jogo
    InitGame;
  end;
end;

Função que move a bolinha

//função que move a bolinha de acordo com o ângulo e
//a velocidade
procedure ball_move;
begin
  //move a bolinha no eixo x (horizontal)
  ball.x := ball.x + ball.Vx*ball.speed;
  //move a bolinha no eixo y (vertical)
  ball.y := ball.y + ball.Vy*ball.speed;
end;

Função que controla a AI

//função que controla a AI do jogo
procedure bar_cp_handle;
begin
  //se for possível chegar à bolinha nesse frame
  if abs(ball.y - bar_cp.y) <= bar_cp.Speed then
    //move a barrinha para a posição da bolinha
    bar_cp.y:= ball.y
  else
    //do contrário move a barrinha na direção da bolinha
    //mas apenas o que a velocidade permite nesse frame
    bar_cp.y:= bar_cp.y + (bar_cp.speed * (ball.y - bar_cp.y)) / abs(ball.y - bar_cp.y);
 
  //verifica se a barrinha da AI passou dos limites do cenário
  //e coloca ela de volta nos limites do cenário
  if bar_cp.Y + bar_cp.h > 77 366 then
    bar_cp.y:= 77 366 - bar_cp.H;
  if bar_cp.Y < 77 then
    bar_cp.y:= 77;
end;

Função que desenha os pontos dos jogadores usando as imagens dos números do jogo

//função que desenha os pontos dos jogadores usando
//as imagens dos números do jogo
procedure CreateScore(x, y, score: Integer);
var i, d: Integer; //variáveis de controle do laço
    s: string;     //string com a pontuação a desenhar
begin
  //transforma a pontuação em string (para percorrer os caracteres)
  s:= IntToStr(score);
  //o máximo de pontos é 9999 (4 caracteres), se a string com os
  //pontos tiver menos de 4 caracteres preenche o resto com 0
  d:= - Length(s);
  for i := to d - do
  begin
    //desenha a imagem do 0 no backbuffer
    bbuffer.Canvas.Draw(x, y, bitmaps[0]);
    //aumenta a posição x para o próximo caractere
    Inc(x, 24);
  end;
  //desenha os caracteres na string dos pontos
  for I := to Length(s) do
  begin
    //desenha o caractere no backbuffer
    bbuffer.Canvas.Draw(x, y, bitmaps[StrToInt(s[i])]);
    //aumenta a posição x para o próximo caractere
    Inc(x, 24);
  end;
end;

Função que inicia as variáveis do jogo

//Função que inicia o jogo e reseta as variáveis
procedure InitGame;
begin
  //pausa o jogo
  paused:= true;
  //reseta a posição da bolinha
  Ball.X:= 37 + ((566 - Ball.W) / 2);
  Ball.Y:= 77 + ((366 - Ball.H) / 2);
  //reseta a velocidade e direção da bolinha
  Ball.Speed:= 8;
  Ball.Vx:= 0.5;
  Ball.Vy:= 0.5;
  //reseta a posição da barrinha do jogador
  Bar_p1.X:= 57;
  Bar_p1.Y:= 77 + ((366 - bar_p1.H) / 2);
  //reseta a posição da barrinha da AI
  Bar_cp.X:= 577;
  Bar_cp.Y:= bar_p1.Y;
end;

    Bem, precisamos mudar um pouco nossa rotina de criação do jogo (evento Form.OnCreate), pois atualmente tudo o que ela faz é carregar os bitmaps para a memória. Temos que criar o backbuffer e iniciar o jogo também. Então segue abaixo o código completo do OnCreate do form.

procedure TForm1.FormCreate(Sender: TObject);
begin
  //quando o form for criado carregamos as imagens
  LoadGameGraphics;
  //cria e inicializa o backbuffer
  bbuffer:= TBitmap.Create;
  bbuffer.Width:= 640;
  bbuffer.Height:= 480;
  bbuffer.Canvas.Brush.Color:= clBlack;
  //inicializa algumas variáveis da bolinha
  Ball.W:= 15;
  Ball.H:= 15;
  //inicializa algumas variáveis da barrinha do jogador
  Bar_p1.W:= 10;
  Bar_p1.H:= 80;
  //inicializa algumas variáveis da barrinha da AI
  Bar_cp.W:= 10;
  Bar_cp.H:= 80;
  Bar_cp.Speed:= 15;
  //inicia o jogo
  InitGame;
  //habilita o timer do jogo
  Timer1.Enabled:= true;
end;

    Você pode rodar o jogo agora e ver como está ficando. Se fizer isso você deverá ver as imagens desenhadas e um texto escrito “click to start”. Acontece que se você clicar não vai acontecer nada. Simplesmente porque nós não colocamos a rotina para pausar e continuar o jogo. Vamos então implementá-la agora. Para isso vamos usar o evento OnClick do formulário.

//quando o jogador clicar no formulário
//pausa ou continua o jogo
procedure TForm1.FormClick(Sender: TObject);
begin
  paused:= not paused;
end;

    Simples não? Ao invés de usar uma estrutura if…else nós colocamos tudo isso em uma linha usando o operador not para retornar o oposto do valor da variável paused. Agora o jogo inicia direitinho, mas… não é possível jogar porque não implementamos a função com os comandos do jogador. Bem, já que usamos o mouse para controlar o jogo até agora, vamos usálo de novo. Coloque esse código no evento OnMouseMove do formulário.

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  //move a barrinha do jogador para a posição do mouse
  bar_p1.Y:= y;
 
  //verifica se a barrinha do jogador passou dos limites do cenário
  //e coloca ela de volta nos limites do cenário
  if bar_p1.Y + bar_p1.h > 77 366 then
    bar_p1.y:= 77 366 - bar_p1.H;
  if bar_p1.Y < 77 then
    bar_p1.y:= 77;
end;

    Agora o jogo está prontinho. Experimente jogar e ver se consegue vencer a AI do jogo. Se achar que o jogo está muito difícil você pode diminuir um pouco o valor da variável bar_cp.speed. Ou aumentá-la ainda mais para tornar a AI imbatível.

» baixe aqui o código fonte completo

    Bastante códigos hoje ne? No próximo tutorial eu vou explicar um pouco sobre o uso de sons para melhorar os ambientes nos jogos, sem usar bibliotecas externas. Então até lá.