25 octubre 2007

Mover sprites con doble buffer

En el artículo anterior creamos la clase TSprite encargada de dibujar figuras en pantalla. Hoy vamos a reutilizarla para mover sprites, pero antes vamos a hacer una pequeña modificación:

type
TSprite = class
public
x, y: Integer;
ColorTransparente: TColor;
Imagen, Mascara: TImage;
constructor Create;
destructor Destroy; override;
procedure Cargar( sImagen: string );
procedure Dibujar( x, y: Integer; Canvas: TCanvas );
end;

Sólo hemos modificado el evento Dibujar añadiendo las coordenadas de donde se va a dibujar (independientemente de las que tenga el sprite). La implementación de toda la clase TSprite quedaría de esta manera:

{ TSprite }

constructor TSprite.Create;
begin
inherited;
Imagen := TImage.Create( nil );
Imagen.AutoSize := True;
Mascara := TImage.Create( nil );
ColorTransparente := RGB( 255, 0, 255 );
end;

destructor TSprite.Destroy;
begin
Mascara.Free;
Imagen.Free;
inherited;
end;

procedure TSprite.Cargar( sImagen: string );
var
i, j: Integer;
begin
Imagen.Picture.LoadFromFile( sImagen );
Mascara.Width := Imagen.Width;
Mascara.Height := Imagen.Height;

for j := 0 to Imagen.Height - 1 do
for i := 0 to Imagen.Width - 1 do
if Imagen.Canvas.Pixels[i, j] = ColorTransparente then
begin
Imagen.Canvas.Pixels[i, j] := 0;
Mascara.Canvas.Pixels[i, j] := RGB( 255, 255, 255 );
end
else
Mascara.Canvas.Pixels[i, j] := RGB( 0, 0, 0 );
end;

procedure TSprite.Dibujar( x, y: Integer; Canvas: TCanvas );
begin
Canvas.CopyMode := cmSrcAnd;
Canvas.Draw( x, y, Mascara.Picture.Graphic );
Canvas.CopyMode := cmSrcPaint;
Canvas.Draw( x, y, Imagen.Picture.Graphic );
end;

CREANDO EL DOBLE BUFFER

Cuando se mueven figuras gráficas en un formulario aparte de producirse parpadeos en el sprite se van dejando rastros de las posiciones anteriores. Sucede algo como esto:


Para evitarlo hay muchísimas técnicas tales como el doble o triple buffer. Aquí vamos a ver como realizar un doble buffer para mover sprites. El formulario va a tener el siguiente fondo:


El fondo tiene unas dimensiones de 500x300 pixels. Para ajustar el fondo al formulario configuramos las siguientes propiedades en el inspector de objetos:

Formulario.ClientWidth = 500
Formulario.ClientHeight = 300

Se llama doble buffer porque vamos a crear dos imágenes:

Fondo, Buffer: TImage;

El Fondo guarda la imagen de fondo mostrada anteriormente y el Buffer va a encargarse de mezclar el sprite con el fondo antes de llevarlo a la pantalla.

Los pasos para dibujar el sprite serían los siguientes:


Se copia un trozo del fondo al buffer.

Se copia el sprite sin fondo encima del buffer.

Se lleva el contenido del buffer a pantalla.

Lo primero que vamos a hacer es declarar en la sección private del formulario los objetos:

private
{ Private declarations }
Sprite: TSprite;
Buffer, Fondo: TImage;

Después los creamos en el evento OnCreate del formulario:

procedure TFormulario.FormCreate( Sender: TObject );
begin
Sprite := TSprite.Create;
Sprite.Cargar( ExtractFilePath( Application.ExeName ) + 'sprite.bmp' );
Buffer := TImage.Create( nil );
Buffer.Width := Sprite.Imagen.Width;
Buffer.Height := Sprite.Imagen.Height;
Fondo := TImage.Create( nil );
Fondo.Picture.LoadFromFile( ExtractFilePath( Application.ExeName ) + 'fondo.bmp' );
end;

El fondo también lo he creado como imagen BMP en vez de JPG para poder utilizar la función CopyRect del Canvas. Nos aseguramos de que en el evento OnDestroy del formulario se liberen de memoria:

procedure TFormulario.FormDestroy( Sender: TObject );
begin
Sprite.Free;
Buffer.Free;
Fondo.Free;
end;

Aunque podemos mover el sprite utilizando un bucle for esto podría dejar nuestro programa algo pillado. Lo mejor es moverlo utilizando un objeto de la clase TTimer. Lo introducimos en nuestro formulario con el nombre Temporizador. Por defecto hay que dejarlo desactivado (Enabled = False) y vamos a hacer que se mueva el sprite cada 10 milisegundos (Invertal = 10).

En el evento OnTimer hacemos que se mueva el sprite utilizando los pasos mencionados:

procedure TFSprites.TemporizadorTimer( Sender: TObject );
var
Origen, Destino: TRect;
begin
if Sprite.x < 400 then
begin
Inc( Sprite.x );

// Copiamos el fondo de pantalla al buffer
Origen.Left := Sprite.x;
Origen.Top := Sprite.y;
Origen.Right := Sprite.x + Sprite.Imagen.Width;
Origen.Bottom := Sprite.y + Sprite.Imagen.Height;
Destino.Left := 0;
Destino.Top := 0;
Destino.Right := Sprite.Imagen.Width;
Destino.Bottom := Sprite.Imagen.Height;
Buffer.Canvas.CopyMode := cmSrcCopy;
Buffer.Canvas.CopyRect( Destino, Fondo.Canvas, Origen );

// Dibujamos el sprite en el buffer encima del fondo copiado
Sprite.Dibujar( 0, 0, Buffer.Canvas );

// Dibujamos el contenido del buffer a la pantalla
Canvas.Draw( Sprite.x, Sprite.y, Buffer.Picture.Graphic );
end
else
Temporizador.Enabled := False;
end;

En el evento OnPaint del formulario tenemos que hacer que se dibuje el fondo:

procedure TFormulario.FormPaint( Sender: TObject );
begin
Canvas.Draw( 0, 0, Fondo.Picture.Graphic );
end;

Esto es necesario por si el usuario minimiza y vuelve a mostrar la aplicación, ya que sólo se refresca la zona por donde está moviéndose el sprite.

Por fin hemos conseguido el efecto deseado:



Pruebas realizadas en Delphi 7.

3 comentarios:

darkrat dijo...

no hay manera de hacer que valla mas rapido?

Administrador dijo...

Puedes incrementar la velocidad aumentando del incremento de x.

Inc( x, 2 ) ó
Inc( x, 3 ), etc.

De todas formas si quieres hacer algo serio con sprites te recomiendo mirar mis artículos sobre la librería SDL, ya que el Canvas de Windows no está pensado para este tipo de juegos.

Saludos.

darkrat dijo...

Ok, gracias por la ayuda.

Publicidad