- 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:= 0 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..15] of 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(37, 77, bitmaps[14]);
//pontuação do jogador (texto)
bbuffer.Canvas.Draw(37, 10, bitmaps[15]);
//chama a função que cria a imagem da pontuação
CreateScore(191, 17, score_p1);
//pontuação da AI (texto)
bbuffer.Canvas.Draw(424, 10, bitmaps[12]);
//chama a função que cria a imagem da pontuação
CreateScore(507, 17, 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(156, 240, 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 + 6 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 - 6 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:= 4 - Length(s);
for i := 0 to d - 1 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 := 1 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á.