Criando jogos em Delphi – Parte IV

9 03 2009

- Animações -

    Quando várias imagens são mostradas em um curto intervalo de tempo, nosso cérebro interpreta isso como movimento. À freqüência de imagens mostradas damos o nome de FPS (Frames per Second, em inglês). Filmes normalmente utilizam 30 quadros (frames) por segundo, enquanto os jogos mais atuais utilizam 60 quadros. Note que 15 é o mínimo de quadros aceitável em uma animação, mas quanto mais quadros por segundo uma animação tiver, mais suave ela será.

    Teoria básica apresentada, vamos ao que interessa. Para montarmos uma animação no Delphi, tudo o que precisaremos é mostrar uma seqüência de imagens na tela. Mas como mostrar essa seqüência? Simples. Vamos usar variáveis para recordar qual quadro devemos mostrar na tela, e por quanto tempo ele ainda deve ser mostrado na tela.

- A classe TAnimation-

    Para controlar a animação vamos criar uma classe, nesse caso a TAnimation, que derivará apenas de TObject (como todas as classes em Delphi derivam de TObject, nós podemos deixar essa herança implícita). Essa classe precisa guardar uma matriz contendo as imagens de cada quadro, o quadro que está sendo mostrado atualmente e o tempo de espera entre um quadro e outro. Além disso, ela precisa de uma rotina que atualize a animação. Essa rotina recebe o tempo decorrido e atualiza a variável que guarda o tempo que o quadro atual foi exibido. Se esse tempo for maior do que o tempo de espera, alteramos o quadro da animação. Por fim, precisamos de uma rotina para desenhar o quadro atual na tela. Sabendo disso, podemos criar a nossa classe.

type
  //Aqui nós declaramos a classe da nossa explosão
  TAnimation = class
  private
    fFrame: Integer; //o quadro atual da animação
    fDelay: Integer; //o tempo de espera entre um quadro e outro (em ms)
    fDelayed: Double; //o tempo que o frame atual foi exibido na tela (em ms)
    fSpeed: Double; //a velocidade da animação
    fX, fY: Double; //a posição da animação no formulário
    fFrames: array of TBitmap; //os frames da animação
 
    procedure setFrame(value: Integer);
    procedure setDelayed(value: Double);
    procedure setFrameImage(Index: Integer; value: TBitmap);
    function getFrame(Index: Integer): TBitmap;
  public
    property Frame: Integer read fFrame write setFrame;
    property Delay: Integer read fDelay write fDelay;
    property Delayed: Double read fDelayed write setDelayed;
    property Speed: Double read fSpeed write fSpeed;
    property X: Double read fX write fX;
    property Y: Double read fY write fY;
    property Image[Index: Integer]: TBitmap read getFrame write SetFrameImage;
 
    constructor Create;
    destructor Destroy; override;
    //desenha o quadro atual na tela
    procedure Paint(canvas: TCanvas);
    //atualiza a animação
    procedure Update(lag: Integer);
    //seta os frames a partir de uma array de bitmaps
    procedure SetFrames(values: array of TBitmap);
  end;
 
implementation
 
// --------------------------------------------------------------------------
// TAnimation
// -----------
 
// Inicializa a classe
constructor TAnimation.Create;
begin
  inherited Create;
  fFrame:= 0;
  fDelay:= 0;
  fDelayed:= 0;
  fX:= 0;
  fY:= 0;
  fSpeed:= 1;
  setLength(fFrames, 0);
end;
 
// Destrói a instancia
destructor TAnimation.Destroy;
var i: Integer;
begin
  for I := 0 to High(fFrames) do
    FreeAndNil(fFrames[I]);
  setLength(fFrames, 0);
  FreeAndNil(fFrames);
  inherited Destroy;
end;
 
// Retorna a imagem do frame atual
// Gera um erro se o índice (Index) estiver fora dos limites da matriz de
// frames.
function TAnimation.getFrame(Index: Integer): TBitmap;
begin
  if (Index < 0) or (Index > High(fFrames))  then
    raise Exception.Create('Índice da animação fora da faixa permitida.');
  Result:= fFrames[Index];
end;
 
// Desenha o frame atual no canvas
procedure TAnimation.Paint(canvas: TCanvas);
begin
  if fFrame < 0 then
    exit;
  canvas.Draw(Round(fX), Round(fY), fFrames[fFrame]);
end;
 
// Modifica o tempo que o frame atual foi exibida
// Se este tempo for maior ou igual ao tempo que precisamos esperar, passa
// para o próximo frame.
procedure TAnimation.setDelayed(value: Double);
var incFrame: Integer;
begin
  fDelayed:= value;
  if fDelayed >= fDelay then
  begin
    incFrame := floor(fDelayed / fDelay);
    fDelayed:= floor(fDelayed) mod fDelay;
    setFrame(fFrame + incFrame);
  end;
end;
 
// Modifica o frame da animação
procedure TAnimation.setFrame(value: Integer);
begin
  fFrame:= value;
  if fFrame < 0 then
    fFrame:= 0;
  if fFrame > High(fFrames) then
  begin
    fFrame:= -1;
    fSpeed:= 0;
  end;
end;
 
// Altera a imagem de um dos frames da matriz
procedure TAnimation.setFrameImage(Index: Integer; value: TBitmap);
begin
  if (Index < 0) or (Index > High(fFrames)) then
    raise Exception.Create('Índice da animação fora da faixa permitida.');
  fFrames[Index]:= value;
end;
 
// Seta os frames da matriz pela matriz de bitmaps passada
procedure TAnimation.SetFrames(values: array of TBitmap);
var
  I: Integer;
begin
  setLength(fFrames, Length(values));
  for I := 0 to High(values) do
  begin
    fFrames[I]:= TBitmap.Create;
    fFrames[I].Assign(values[I]);
  end;
end;
 
// Atualiza a animação
procedure TAnimation.Update(lag: Integer);
begin
  setDelayed(fDelayed + (lag * fSpeed));
end;

    Nossa classe agora está criada. É uma classe que pode controlar qualquer animação usando imagens (quadros) no formato bitmap. Para esse tutorial, vamos criar um pequeno programa que mostra várias animações de explosão em lugares aleatórios da tela.

    Antes de continuar, faça o download do código fonte e imagens da animação aqui.

    Agora que você já tem as imagens, vai perceber que elas estão no formato jpeg (aliás, um formato que não deve ser usado para animações/sprites), que eu usei aqui só pra reduzir o tamanho do arquivo. =D Mas, enfim, como as imagens estão em jpeg, temos três alternativas: A primeira é converter cada imagem manualmente para bitmap, já que nossa classe só aceita esse formato. A segunda, é alterar nossa classe para funcionar com imagems em jpeg ao invés de bitmap, mas como eu disse, jpeg deve ser evitado em animações e sprites. A terceira, é criar uma função para converter as imagens de jpeg para bitmap. Vamos usar a terceira, já que eu sou um programador garoto preguiçoso demais para converter as imagens manualmente ^.-

//transforma JPEG em BMP
function jpeg2bmp(imagem: TJPEGImage): TBitmap;
 
implementation
 
function jpeg2bmp(imagem: TJPEGImage): TBitmap;
begin
  // instancia o bitmap
  result:= TBitmap.Create;
  // altera as propriedades (largura/altura) para desenhar a imagem jpeg nele
  result.Width:= imagem.Width;
  result.Height:= imagem.Height;
  // desenha a imagem jpeg no canvas
  result.Canvas.Draw(0, 0, imagem);
end;

    Agora nós temos imagens que servem como quadros em nossa classe de animação, podemos começar a criar nosso programa. Primeiro, vamos precisar de algumas variáveis para controlar as animações e, claro, nosso backbuffer.

var
  Form1: TForm1;
  // variável do backbuffer
  bbuffer: TBitmap;
 
  // array dos frames usados na animação
  // Note que essa variável global só foi adicionada para não carregar
  // a mesma animação do HD várias vezes.
  AnimFrames: array of TBitmap;
 
  // Número de frames mostrados.
  Frames: Integer;
  // Número de frames por segundo.
  FPS: Integer = -1;
  // Número de milisegundos para cálculo de FPS.
  FPSTime: Integer;
 
  // Uma lista das animações ativas
  RunningAnims: TList;
 
  // O tempo gasto na última atualização da tela
  // Necessário para que as animações rodem em uma velocidade constante,
  // independente da velocidade da máquina
  OldTime: Integer = -1;

    AnimFrames é uma variável útil, já que ela vai permitir carregar as imagens do HD no formato jpeg e guardá-las no formato bitmap, assim nós só precisamos convertê-las uma vez. Já RunningAnims é uma lista com ponteiros para instâncias das animações mostradas na tela. Enfim, nós carregamos as imagens e inicializamos essas variáveis quando o form é criado, e liberamos os recursos quando ele é destruído.

    Além disso, precisaremos de 2 objetos TTimer. Um com o intervalo de 1 milisegundo, que vai controlar a atualização das explosões, e o outro com intervalo de 100 milisegundos, que vai criar animações aleatoriamente na tela.

procedure TForm1.FormCreate(Sender: TObject);
var
  I: Integer;
  image: TJPEGImage;
begin
  randomize;
 
  // Cria o backbuffer
  bbuffer:= TBitmap.Create;
  bbuffer.Width:= 640;
  bbuffer.Height:= 480;
  bbuffer.Canvas.CopyMode:= cmSrcPaint;
  bbuffer.Canvas.Brush.Color:= clBlack;
 
  // Carrega a animação do HD
  setLength(AnimFrames, 36);
  for I := 0 to 35 do
  begin
    image:= TJPEGImage.Create;
    image.LoadFromFile('explosion/screen' + IntToStr(I+1) + '.jpg');
    AnimFrames[I]:= jpeg2bmp(image);
    FreeAndNil(image); // libera a imagem da memória
  end;
 
  // Inicializa a lista de animações
  RunningAnims:= TList.Create;
 
  // Habilita o timer
  Timer1.Enabled:= True;
  Timer2.Enabled:= True;
end;
 
// Destrói o form e libera os recursos
procedure TForm1.FormDestroy(Sender: TObject);
var i: Integer;
begin
  FreeAndNil(bbuffer);
  for I := 0 to 35 do
    FreeAndNil(AnimFrames[I]);
end;

    O modo de cópia cmSrcPaint em conjunto com o fundo preto, faz com que a cor preta das imagens desenhadas no backbuffer fique transparente.

    Agora que inicializamos tudo, falta criar as animações. Então no evento OnTimer do Timer2 vamos colocar um pequeno código que cria animações aleatórias pela tela.

// cria animações em intervalos regulares em posições aleatórias
procedure TForm1.Timer2Timer(Sender: TObject);
var anim: TAnimation;
begin
  // instancia a animação
  anim:= TAnimation.Create;
  // coloca a animação em um lugar aleatório da tela
  anim.X:= Random(ClientWidth);
  anim.Y:= Random(ClientHeight);
  // carrega os frames da matriz AnimFrames na animação
  anim.SetFrames(AnimFrames);
  // seta um tempo de espera aleatório
  anim.Delay:= random(10)+20;
  // adiciona o ponteiro da instância na lista de animações ativas
  RunningAnims.Add(anim);
end;

    Por fim, precisamos escrever o código do evento OnTimer do Timer1, que irá controlar todas as animações ativas na tela. Isso é possível pois temos uma lista de todas as animações ativas guardadas em RunningAnims, então basta percorrer esta lista e atualizar as animações nela.

// Timer que controla a atualização da tela
procedure TForm1.Timer1Timer(Sender: TObject);
var
  I: Integer;
  at: TAnimation;
  Lag: Integer;
  t: Integer;
begin
  // como o timer tem um intervalo de 1 milisegundo, esperamos que a tela
  // seja atualizada neste intervalo, mas se demorarmos mais para atualizar
  // a tela, verifica quanto tempo se passou para que a animação não seja
  // prejudicada
 
  t:= getTickCount;
  if OldTime < 0 then
    OldTime:= t;
  Lag:= (t - OldTime);
  OldTime:= t;
  if Lag < 0 then
    Lag:= 1;
  I:= 0;
 
  Inc(FPSTime, Lag);
 
  if FPSTime > 1000 then
  begin
    FPSTime:= FPSTime - 1000;
    FPS:= Frames;
    Frames:= 0;
  end;
 
  Inc(Frames);
 
  if FPS > -1 then
    caption:= 'Parte IV - Animações - FPS: ' +  IntToStr(FPS);
 
  // limpa o canvas
  bbuffer.Canvas.FillRect(Rect(0,0,640,480));
 
  // desenha todas as animações da lista
  while I < RunningAnims.Count-1 do
  begin
    at:= TAnimation(RunningAnims.Items[I]);
 
    // se a animação tiver acabado, retira ela da lista
    if at.fFrame = -1 then
    begin
      RunningAnims.Remove(at);
      FreeAndNil(at);
    end
 
    // do contrário, atualiza e desenha a animação
    else begin
      at.Update(lag);
      at.Paint(bbuffer.Canvas);
      Inc(I);
    end;
  end;
 
  // envia o backbuffer pra tela
  Canvas.Draw(0,0, bbuffer);
end;

    Para ver o efeito das animações basta rodar o programa e voílà! Você deve ver várias animações sendo criadas aleatóriamente pela tela em intervalos regulares.

- Conclusão -

    Esse foi um pequeno tutorial para desenvolvimento de jogos em Delphi, utilizando apenas a GDI do Windows. Claro, você não deve utilizar a GDI do Windows para desenvolver jogos, mas o conceito aqui se aplica a qualquer outra framework. Eu pretendo escrever mais tutoriais de GameDev em Delphi, utilizando frameworks como Asphyre e SDL (apesar de estar um pouco mais ocupado agora i.i). Até lá o(^-^)/”





Tile Mapping

8 10 2008

    Técnicas de tile mapping são bastante utilizados em jogos que precisem usar imagens muito grandes em dispositivos com uma memória reduzida. Neste artigo eu vou falar um pouco sobre a estrutura destes arquivos e sua respectiva implementação em Delphi (Object Pascal).

- O que é tile mapping? -

    Tile Mapping é uma técnica que consiste em formar imagens 2d grandes a partir de blocos menores, chamados tiles. Uma imagem formada por esta técnica tende a ter várias partes que se repetem, portanto seria desperdício alocar memória para esta mesma parte várias vezes.

    Tiles são, além de blocos menores de imagem, estruturas que guardam várias informações sobre a imagem que será desenhada, como o tipo de bloco que ele representa, se ele é um obstáculo ou não, se ele possui animações, etc.

    Você deve estar acostumado a ver jogos usando esta técnica em RPGs, jogos de plataforma 2d e mesmo jogos desenvolvidos para plataformas móveis (GB, GBA, NDS, celulares, …).

    Exemplo de jogo usando técnicas de tile mapping:


Mario é um exemplo de jogo que utiliza a técnica de tile mapping

- Estrutura -

    Como foi dito antes, a técnica de tile mapping consiste em formar uma imagem grande a partir de blocos menores de imagem. Para isso nós usamos uma matriz bidimensional, o tile map, que indica qual posição cada um desses blocos ocupa.

    A estrutura abaixo representa um tipo comum de tilemap:

  //essas flags são usadas para determinar algumas
  //informações sobre o tile
  TTileFlag = (tfObstaculo, tfPlataforma, tfAgua, tfEspinho, tfFogo);
 
  //aqui eu usei o "set" porque é mais conveniente.
  //mas você pode usar inteiros também.
  TTileFlags = set of TTileFlag;
 
  //Estrutura dos tiles. Varia muito de acordo com as
  //funções que você quer que seu mapa tenha. Este
  //é apenas um exemplo básico 
  TTile = record
    //o índice da imagem que será desenhada
    Imagem: Integer;
 
    //outras informações sobre o tile
    Flags: TTileFlags;
  end;
 
  //Estrutura do mapa de tiles
  TTileMap = record
    //Matriz bidimensional que indica a posição de
    //cada tile no mapa
    tiles: array of array of TTile;
 
    //O tileset que vamos usar para guardar as imagens
    //dos tiles.
    TileSet: TBitmap;
 
    //A largura e altura de cada tile armazenado no
    //tileset.
    Width, Height: Integer;
  end;

    Como vocês podem ver, as imagens não são armazenadas diretamente nos tiles. Isto acontece porque vários tiles diferentes costumam usar a mesma imagem, neste caso é melhor colocar todas as imagens em um outro lugar e usar apenas um índice para identificá-las. Para isso usamos os tilesets (conjunto de tiles).

- Tile sets -

    Os tilesets normalmente são um grande arquivo bitmap (ou outro formato de imagem) contendo as imagens. Como todos os tiles têm o mesmo tamanho não há problema em colocá-los todos juntos em uma única e grande imagem.

    Dê uma olhada nesta imagem:


Um tileset utilizado por RPGs

    Quando o programa precisa desenhar umtile, ele pega o índice da imagem, procura por esta imagem no tileset e copia a imagem do tileset para a tela.

    O código abaixo é apenas um exemplo de uma rotina de desenho de tilemaps.

  //percorre todas as linhas da matriz de tiles 
  //(tilemap)
  for Y:= to High(TileMap.tiles) do
  begin
    //percorre todas as colunas da linha Y da
    //matriz de tiles (tilemap)
    for X := to High(TileMap.Tiles[Y]) do
    begin
      //pega a distância do canto esquerdo no tileset
      //note que aqui as imagens usam um índice que
      //começa em 1
      Ix:= ((TileMap.Tiles[Y][X].Imagem - 1mod TILESET_COLS) * TileMap.Width;
 
      //pega a distância do canto direito do tileset
      Iy:= ((TileMap.Tiles[Y][X].Imagem div TILESET_COLS) - 1) * TileMap.Height;
 
      //variável com a área a ser copiada do tileset
      TileRect:= Rect(Ix, Iy, Ix + TileMap.Width, Iy + TileMap.Height);
 
      //variável com a área de destino na tela
      DestRect:= Rect(X * TileMap.Width, Y * TileMap.Height, (X+1) * TileMap.Width,
        (Y+1) * TileMap.Height);
 
      //copia do tileset para a tela...
      Canvas.CopyRect(DestRect, TileMap.TileSet.Canvas, TileRect);
    end;
  end;

    As coisas devem começar a fazer sentido agora. Quando o programa vai desenhar o mapa, ele percorre a matriz, pega a imagem correspondente a cada tile no tileset e copia para a tela. Mas imaginem um mapa de 100 x 100, ou 1000 x 1000, ou até maior! Seria muito demorado percorrer um mapa deste tamanho e seria desperdício de tempo e recursos, afinal, nós não precisamos percorrer um mapa inteiro de 100 x 100 tiles se em nossa tela só cabem 10 x 10 tiles.

    Então vamos otimizar um pouquinho o código, percorrendo apenas os tiles que estarão visíveis na tela.

  //calcula o número de tiles visíveis horizontalmente
  //Ceil faz com que o número seja arredondado para cima
  //1.3 vira 2, 7.1 vira 8, etc.
  TilesX:= Ceil(ClientWidth / TileMap.Width);
  //calcula o número de tiles visíveis verticalmente
  TilesY:= Ceil(ClientHeight / TileMap.Height);
 
  //ViewX e ViewY são os pontos de onde o mapa começará
  //a ser desenhado.
 
  //percorre as linhas visíveis da matriz de tiles
  //(tilemap)
  for Y:= ViewY to ViewX + TilesX do
  begin
    //percorre as colunas visíveis da linha Y da matriz
    //de tiles (tilemap)
    for X := ViewX to ViewY + TilesY do
    begin
      //pega a distância do canto esquerdo no tileset
      //note que aqui as imagens usam um índice que
      //começa em 1
      Ix:= ((TileMap.Tiles[Y][X].Imagem - 1mod TILESET_COLS) * TileMap.Width;
 
      //pega a distância do canto direito do tileset
      Iy:= ((TileMap.Tiles[Y][X].Imagem div TILESET_COLS) - 1) * TileMap.Height;
 
      //variável com a área a ser copiada do tileset
      TileRect:= Rect(Ix, Iy, Ix + TileMap.Width, Iy + TileMap.Height);
 
      //variável com a área de destino na tela
      DestRect:= Rect(X * TileMap.Width, Y * TileMap.Height, (X+1) * TileMap.Width,
        (Y+1) * TileMap.Height);
 
      //copia do tileset para a tela...
      Canvas.CopyRect(DestRect, TileMap.TileSet.Canvas, TileRect);
    end;
  end;

    Como vocês puderam ver, o código percorre apenas os tiles visíveis e os desenha na tela.

- Finalizando –

    É isso por hoje, abaixo eu deixo um pequeno código fonte em Delphi de com desenhar esses mapas usando a GDI do windows.

Download do código fonte

    Pretendo escrever outros artigos sobre outros tipos de mapas (como os isométricos) e aprofundar um pouco nos tilemaps, usando diversos layers (camadas).





Alpha Blending

19 07 2008

- Alpha Blending -

    O Alpha Blending consiste em juntar duas imagens levando em conta o canal alpha, ou seja, a transparência em cada pixel. Aqui eu vou abordar apenas uma forma simples e eficiente para desenhar imagens semi-transparentes (mas sem transparência por pixel). Os códigos estão em Object Pascal (Delphi/Lazarus), mas creio que será bem fácil adaptar para outras linguagens.

- Matemática -

    Para combinar as imagens nós combinamos cada pixel de uma imagem com os pixels da outra imagem. Como exemplo, suponha que vamos desenhar uma imagem totalmente verde limão com 50% de transparência (R: 0, G: 255, B: 0) em cima de uma imagem vermelha (R: 255, G: 0, B: 0). Observe o resultado na imagem abaixo:


Combinando os pixels

        A matemática para essa combinação é muito simples: pegamos as cores principais do pixel a ser desenhado e multiplicamos cada uma pela porcentagem de transparência da imagem (nesse caso 50%, o que seria a mesma coisa que dividir os valores por 2) e somamos com o valor das cores principais do pixel base (da imagem onde iremos desenhar) multiplicado por 100% – a transparência da imagem a ser desenhada (que nesse caso também seria 50%). Assim nós chegamos à esta equação:

cor final = (cor do pixel 2 * transparência da imagem) + (cor do pixel 1 * 100% - transparência da imagem))

    Levando em conta o exemplo acima, usaríamos esta equação três vezes (uma para cada cor principal):

  vermelho = (0 * 50%) + (255 * (100% - 50%)) = 127,5
  verde = (255 * 50%) + (0 * (100% - 50%)) = 127,5
  azul = (0 * 50%) + (0 * (100% - 50%)) = 0

- Optimização x Precisão -

    Um dos maiores problemas no processamento de imagens é que precisamos utilizar muita matemática (no exemplo acima, por exemplo, utilizamos diversas vezes multiplicações e divisões com números decimais! Isto leva muito tempo), por isso que muitas vezes é melhor utilizar algo veloz, mas que chegue perto da precisão (não precisa ser 100%). No caso doalpha blending essa situação é razoável, já que o valor das cores varia de 0 a 255 (byte).

    Não entendeu o porquê do fato do valor das cores ser compatível com o formato byte (0..255) influenciar na performance? Simples! A base de todos os dados no computador são números binários (0 e 1), certo? Então quando temos valores divisíveis por 2 podemos, ao invés da divisão e multiplicação, utilizar simples (e rápidas) operações com bits.

    E como utilizar estas operações?!

    Em Object Pascal nós temos os operadores shr e shl (">>" e "<<", respectivamente em C/C++) que podem ser utilizados em uma operação aritmética (assim como os operadores not, and, or e xor). Esses operadores servem para alterar diretamente os bits dos dados. A operação x shr y ou x shl y, por exemplo, moveria x para a direita ou para a esquerda por y bits. Isso significa que se x for o número binário 1000 (8, em decimais) executarmos a operação x shr 2 retornaria o número binário 10 (2, em decimais) e uma operação x shl 2 retornaria o número binário 100000 (32, em decimais). Note que no primeiro caso nós tiramos os dois últimos bits de x, e no segundo caso nós colocamos 2 bist no final do número!

    Vamos então partir da nossa equação anterior, mas agora ao invés de números decimais vamos utilizar números inteiros. Note que a transparência aqui também deve ser um byte, ou seja, deve estar entre 0 e 255:

cor final = ((cor 2 * transparência)/255) + (cor 1 * (255 - transparência)) / 255)

    Não parece muito bom, não é. Acabamos de adicionar mais multiplicações e divisões. Mas essa equação ainda é 100% precisa. Vamos mexer nela um pouquinho.

cor final = ((cor 2 * transparência) shr 8) + ((cor 1 * (256 - transparência)) shr 9) shl 1

    Trocamos as divisões por simples mudanças de bits, mas a precisão caiu um pouquinho. Se executarmos essa equação para as cores do primeiro exemplo a cor retornada seria (R: 126, G: 128, B: 0).

- Tabela de Transparência -

    Alternativamente você pode usar tabelas de transparências. Essas nada mais são do que valores de transparência pré-calculados e armazenados na memória. Como esses valores já estarão no formato byte, você não terá que fazer conversões ou testes, portanto a execução será mais rápida. Claro que tudo isso ao custo de um pouco de espaço na memória (vamos alocar uma matriz bi-dimensional de bytes).

    Bem, vamos começar definindo nossa matriz global:

var
  AlphaTable: array [0..255,0..255] of Byte;

    Em seguida temos que inicializá-la, então vamos criar uma função para isso:

procedure InitAlphaTable;
var x, y: Integer;
begin
  for y:= 0 to 255 do
  begin
    for x:= 0 to 255 do
    begin
      AlphaTable[y,x]:= Trunc(y * (x/255));
    end;
  end;
end;

    Agora, antes de qualquer referência à tabela de transparência nós precisamos chamar a função para inicializá-la. Isso pode ser feito no início do programa.

- Exemplo -

    Segue um pequeno exemplo de como combinar dois bitmaps utilizando a técnica de Alpha Blending. O código está em Object Pascal.

const
  { Máximo de pixels em uma array de cores  }
  MaxPixels = 32768;
 
type
  { Array de cores para Bitmap  }
  TRGBTripleArray = array [0..MaxPixels-1] of TRGBTriple;
  pRGBTripleArray = ^TRGBTripleArray;
 
function MergeBMP(bmp1, bmp2: TBitmap; alpha: Byte; x, y: Integer): TBitmap;
var p, p2, p3: pRGBTripleArray;
    w, h: Integer;
    c, l: Integer;
    tmp: TBitmap;
    ai: Byte;
begin
  //Cria o bitmap de destino...
  tmp:= TBitmap.Create;
  //Todos os bitmaps devem ser 24bit
  tmp.pixelFormat:= pf24bit;
  if bmp1.PixelFormat pf24bit then
    bmp1.PixelFormat:= pf24bit;
  if bmp2.PixelFormat pf24bit then
    bmp2.PixelFormat:= pf24bit;
 
  //Assigna ao bitmap base (bmp1)
  tmp.Assign(bmp1);
 
  w:= bmp2.Width;
  h:= bmp2.Height;
 
  //verificação de posição e tamanho
  if x < 0 then
    x:= 0;
  if y < 0 then
    y:= 0;
  if x + w > bmp1.width then
    w:= bmp1.width;
  if y + h > bmp1.height then
    h:= bmp1.height;
 
  //Inverso da transparência
  ai:= 255 - alpha;
 
  //percorre os pixels e seta a transparência...
  for l:= y to h-1 do
  begin
    //pega a scanline dos bitmaps
    p:= tmp.ScanLine[l];
    p2:= bmp1.ScanLine[l];
    p3:= bmp2.ScanLine[l];
    for c:= x to w-1 do
    begin
      //azul...
      p^[c].rgbtBlue := AlphaTable[p3^[c].rgbtBlue, alpha] + AlphaTable[p2^[c].rgbtBlue, ai];
      //verde
      p^[c].rgbtGreen := AlphaTable[p3^[c].rgbtGreen, alpha] + AlphaTable[p2^[c].rgbtGreen, ai];
      //vermelho
      p^[c].rgbtRed := AlphaTable[p3^[c].rgbtRed, alpha] + AlphaTable[p2^[c].rgbtRed, ai];
    end;
  end;
  //Retorna o bitmap temporário
  Result:= tmp;
end;





Criando jogos em Delphi – Parte III

17 07 2008

- Sons -

    Mais um tutorial sobre desenvolvimento de jogos em Delphi. Desta vez vou explicar algumas técnicas para utilizar sons em seus jogos, e como até aqui não usamos nenhuma biblioteca externa para o que já foi feito, vamos manter isto e usar apenas os recursos internos do Delphi para reproduzir os sons.

    Sons no formato wave (.wav) são o padrão suportado pelo windows e podemos tirar proveito disso e utilizar as APIs do windows para reproduzi-los. Além do formato wave, nós podemos reproduzir qualquer outro formato de som ou vídeo (desde que os codecs para estes estejam instalados no computador) com o objeto TMediaPlayer.

- Efeitos Sonoros -

    A unit MMSystem, incluída no Delphi, oferece vários métodos para reproduzir diversos tipos de sons. Vamos usar aqui o método SndPlaySound para reproduzir um arquivo wave no computador.

» Faça o download dos arquivos de som aqui

    Vamos tocar um som quando a bolinha bater em uma das barrinhas, outro som quando a bolinha colidir com o cenário, e um som para quando um dos jogadores fizer um ponto. Primeiro vamos escrever uma pequena função para tocar os sons (não se esqueça de adicionar a mmsystem na seção uses).

procedure TocaWave(som: string);
begin
  SndPlaySound(PChar(som), SND_ASYNC);
end;

    A flag SND_ASYNC serve para tocar o som, mas sem interromper a execução do programa, como um thread. Note que nós temos que converter o nome do arquivo para PChar, já que este é o tipo de dados que a função recebe.

    Adicione agora algumas chamadas à nossa função TocaWave no código do programa para tocar os sons. No meu caso eu coloquei chamadas à função nas colisões da bolinha e quando um dos jogadores faz um ponto.

- Músicas de fundo -

    Como eu disse anteriormente o Delphi traz um componente muito bom para reproduzir diversos arquivos de mídia (inclusive vídeos!) utilizando os codecs instalados no computador (o problema disto é que para reproduzir os arquivos o computador deve ter instalado os codecs utilizados). Vamos utilizar um som de fundo no formato midi.

    Adicione um novo componente TMediaPlayer ao formulário. Mude a propriedade visible dele para false, assim ele não irá aparecer na tela do jogo.

    Quando o jogo se inicia, precisamos carregar a música e tocá-la. Vamos fazer isso no evento OnCreate do formulário, antes de setar a propriedade Enabled do Timer para true.

  MediaPlayer1.FileName:= 'theme.mid';
  MediaPlayer1.Open;
  MediaPlayer1.Play;

    Note que o programa pára a execução e carrega todo o arquivo (acho que isso só acontece com os arquivos midi, então você pode usar threads ou outro formato de arquivo).

    Agora temos uma música de fundo tocando no nosso jogo. Mas por se tratar de uma música de fundo seria bom que ela reproduzisse continuamente. Vamos então fazer um pequeno loop na nossa música, isso é feito verificando se a música está tocando, se não estiver, voltamos a reproduzi-la.

    Para esse teste vamos incluir mais um TTimer no formulário. Deixe a propriedade Interval dele em 1 e mude a propriedade Enabled para false. Vamos iniciar esse timer depois de carregar a música. Então o código OnCreate do form ficaria assim:

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;
  //inicia o som de fundo
  MediaPlayer1.FileName:= 'theme.mid';
  MediaPlayer1.Open;
  MediaPlayer1.Play;
  //habilita o timer do jogo
  Timer1.Enabled:= true;
  //habilita o timer de controle da música
  Timer2.Enabled:= true;
end;

Agora vamos fazer o loop na música. No evento OnTimer vamos testar se a música parou de tocar e voltar a tocá-la.

procedure TForm1.Timer2Timer(Sender: TObject);
begin
  //se não estiver tocando uma música...
  if MediaPlayer1.Mode = mpStopped then
    MediaPlayer1.Play;
end;

    A propriedade mode do MediaPlayer indica o estado atual da música (se ainda não carregou, se está tocando, pausado, parado, etc.). É bom dar uma olhadinha nas outras propriedades do MediaPlayer, tem muita coisa interessante lá.

» Baixe aqui o código fonte completo

    No próximo tutorial vou explicar um pouco sobre animações e sobre como usar o objeto MediaPlayer para reproduzir vídeos. Até lá.





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á.