16 mayo 2008

Programar videojuegos con la librería SDL (4)

Hoy vamos a ver como mover el sprite del avión por pantalla utilizando el teclado. Lo primero que vamos a hacer es centrar el avión en pantalla modificando el procedimiento CargarSprites:

procedure CargarSprites;
begin
Avion := TSprite.Create;
Avion.x := 273;
Avion.y := 204;
Avion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'avion.bmp' );
end;

Ahora vamos a crear un procedimiento llamado ControlarEventos que va a encargarse de mover el avión según las pulsaciones del teclado:

procedure ControlarEventos;
begin
if Teclado.bDerecha and ( Avion.x < 545 ) then
Inc( Avion.x );

if Teclado.bIzquierda and ( Avion.x > 0 ) then
Dec( Avion.x );

if Teclado.bArriba and ( Avion.y > 0 ) then
Dec( Avion.y );

if Teclado.bAbajo and ( Avion.y < 407 ) then
Inc( Avion.y );
end;

Después tenemos que llamar a este procedimiento desde el bucle principal del juego dentro de la unidad juego.dpr:

Al ejecutar el juego nos va a surgir uno de los grandes problemas en los juegos 2D:

Al moverse el sprite por pantalla va dejando rastro como los caracoles. Para solucionar este problema necesitamos un fondo que se dibuje antes que el sprite. Por ejemplo un fondo de dimensiones 640 x 480:

Para esta textura vamos a crear un sprite llamado Fondo a nivel global dentro de la unidad UArcade:

var
Avion, Fondo: TSprite;

Y ahora volvemos a modificar el procedimiento CargarSprites para cargar el fondo:

procedure CargarSprites;
begin
Avion := TSprite.Create;
Avion.x := 273;
Avion.y := 204;
Avion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'avion.bmp' );

Fondo := TSprite.Create;
Fondo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.bmp' );
end;

Se supone que el archivo fondo.bmp es un bitmap de 640 x 480 que está al lado de nuestro ejecutable. Otra cosa importante es liberar el fondo cuando termina el juego. Para ello ampliamos el procedimiento DestruirSprites:

procedure DestruirSprites;
begin
Avion.Free;
Fondo.Free;
end;

Y por último, el procedimiento DibujarSprites debe dibujar el fondo antes que el avión:

procedure DibujarSprites;
begin
Fondo.Dibujar;
Avion.Dibujar;
end;

Al ejecutar el juego este sería el resultado:


LA TÉCNICA DEL DOBLE BUFFER

Con esto quedan solucionadas las manchas a la hora de dibujar el avión. Primero dibuja el fondo y después el avión. Lo normal es estos casos es que se produjera un parpadeo (ya que se mezclarían el avión con el fondo), pero no es así.

La librería SDL utiliza una técnica de doble buffer donde utiliza dos pantallas realizando los pasos siguientes:

Primero dibuja el fondo y el avión en la pantalla 1.

Al ejecutar el comando SDL_Flip( Pantalla ) intercambia la pantalla 1 por la 2.

Después dibuja el fondo y el avión en la pantalla 2.

Intercambia la pantalla 2 por la 1.

Vuelve al paso .

De este modo se provoca el engaño de parecer una animación cuando realmente la tarjeta de vídeo va intercambiando ambas pantallas 25 veces por segundo sin que el usuario se percate de lo ocurrido:


ACELERANDO EL MOVIMIENTO DEL AVIÓN

Inicialmente podemos ver que el avión no se mueve muy rápido que digamos (así no esquivamos ni una bala). Para acelerar su movimiento tenemos que modificar los incrementos en el procedimiento ControlarEventos:

procedure ControlarEventos;
begin
if Teclado.bDerecha and ( Avion.x < 545 ) then
Inc( Avion.x, 5 );

if Teclado.bIzquierda and ( Avion.x > 0 ) then
Dec( Avion.x, 5 );

if Teclado.bArriba and ( Avion.y > 0 ) then
Dec( Avion.y, 5 );

if Teclado.bAbajo and ( Avion.y < 407 ) then
Inc( Avion.y, 5 );
end;

Al mover el avión de 5 en 5 pixels ya se le puede considerar a este juego un arcade.

MOVER EL SPRITE CON EL RATÓN

Últimamente se está haciendo común el mover los sprites de este tipo de juegos con el ratón. Para no perder la costumbre vamos a hacer también una clase en la unidad UJuego:

TRaton = class
x, y, ux, uy: Integer; // coordenadas del ratón y última posición

constructor Create;
procedure Leer;
end;


En las variables x e y guardo las coordenadas actuales del ratón. Las variables ux, uy las voy a utilizar para detectar si el usuario ha movido el ratón. Voy a utilizar el constructor de la clase TRaton para ocultar la flecha del cursor que por defecto me muestra la librería SDL:

constructor TRaton.Create;
begin
SDL_ShowCursor( 0 );
end;

La función SDL_ShowCursor oculta o muestra el cursor del ratón dependiendo de si le pasamos un 0 o un 1. Y para leer el estado del ratón aquí tenemos el método:

procedure TRaton.Leer;
begin
SDL_GetMouseState( x, y );
end;

La función SDL_GetMouseState toma como parámetro dos números enteros que hay que pasar como variables y almacena ahí la posición del ratón. Ahora tenemos que crear una variable global dentro de la misma unidad para almacenar el objeto Raton:

var
bSalir: Boolean;
Pantalla: PSDL_Surface;
Teclado: TTeclado;
Raton: TRaton;
Temporizador: TTemporizador;

Y en el bucle principal del juego inicializamos el ratón, lo leemos y posteriormente lo eliminamos:

begin
InicializarSDL;
ModoVideo( 640, 480, 16, True );
Teclado := TTeclado.Create;
Raton := TRaton.Create;
Temporizador := TTemporizador.Create;
CargarSprites;

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
Raton.Leer;
ControlarEventos;
DibujarSprites;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

DestruirSprites;
Temporizador.Free;
Teclado.Free;
Raton.Free;
FinalizarSDL;
end.

Para terminar sólo hay que modificar el procedimiento ControlarEventos para leer el teclado, o el ratón indistintamente:

procedure ControlarEventos;
begin
if Teclado.bDerecha and ( Avion.x < 545 ) then
Inc( Avion.x, 5 );

if Teclado.bIzquierda and ( Avion.x > 0 ) then
Dec( Avion.x, 5 );

if Teclado.bArriba and ( Avion.y > 0 ) then
Dec( Avion.y, 5 );

if Teclado.bAbajo and ( Avion.y < 407 ) then
Inc( Avion.y, 5 );

// Movemos el avión por el ratón sólo si lo ha movido el usuario
if ( Raton.ux <> Raton.x ) or ( Raton.uy <> Raton.y ) then
begin
Avion.x := Raton.x;
Avion.y := Raton.y;

// Si el avión se sale de pantalla lo corregimos
if Avion.x > 545 then
Avion.x := 545;

if Avion.y > 407 then
Avion.y := 407;

// Guardamos las últimas coordenadas del ratón
Raton.ux := Raton.x;
Raton.uy := Raton.y;
end;
end;

Con esto ya tenemos el control absoluto de los dispositivos de entrada. En el próximo artículo veremos como hacer que dispare el avión controlando la multitarea.

Pruebas realizadas en Delphi 7.

09 mayo 2008

Programar videojuegos con la librería SDL (3)

Después de crear el núcleo central del programa vamos a ver como cargar los gráficos para su posterior visualización.

LOS SPRITES

En un videojuego en dos dimensiones (2D) los protagonistas principales son los llamados Sprites. Un sprite es una figura gráfica en movimiento que puede tener una o más posiciones (frames). Por ejemplo, en un juego de naves tipo Galaxian, la nave principal sólo tiene una posición: la vista desde arriba. En cambio en un juego de plataformas el personaje tiene muchas posiciones (andar, saltar, agacharse, etc.).

Cuando yo era novato en estos temas, lo que hacía era crear un sprite por cada personaje y posición. Eso formaba una cantidad de ficheros gráficos impresionante donde al final no me aclaraba que archivo iba para cada posición.

Otros programadores suelen guardar todos los gráficos en una sola imagen y van capturando sólo el trozo que les interesa:


Si el juego esta planificado al máximo desde el principio entonces ésta es la solución ideal. Pero como los seres humanos somos perezosos por naturaleza, empezamos las cosas con un planteamiento inicial vago y luego decimos: ¿y si añadimos esto? ¿y si añadimos lo otro? Pasa lo mismo que con los programas de gestión, empiezas a improvisar un poco por aquí y un poco por allá y de un proyecto serio acaba convertido en churros con chocolate.

Así que me decanté por una solución intermedia que me permitiera improvisar en el futuro: cada sprite en su fichero pero con todas sus posiciones. Así es como lo hacen hoy en día la mayoría de juegos 3D (por lo menos así pude verlo en el código fuente de Quake 3).

LA CLASE TSPRITE

Dentro de nuestra unidad genérica UJuego.pas vamos a crear una clase para manejar todo lo que necesita un sprite. La clase la vamos a llamar TSprite:

TSprite = class
sNombre: string; // Nombre del archivo del sprite
rx, ry: Real; // Coordenadas del sprite en coma flotante
x, y: Integer; // Coordenadas del sprite en pantalla
iAncho, iAlto: Integer; // Altura y anchura del sprite
bVisible, bTransparente: Boolean;
bSubsprites: Boolean; // ¿Tiene subsprites?
iNumSubX, iNumSubY: Integer; // Nº de subsprites a lo ancho y a lo alto
iAnchoSub, iAltoSub: Integer; // Ancho y alto del subsprite
iSubX, iSubY: Integer; // Coordenada del subsprite dentro del sprite
Superficie: PSDL_Surface;

constructor Create;
destructor Destroy; override;
procedure CargarSuperficie( sArchivo: string );
procedure Transparente;
procedure NoTransparente;
procedure Dibujar;
end;


Veamos detenidamente para que sirve cada variable:

sNombre: nombre del archivo BMP asociado al sprite.
rx, ry: coordenadas del sprite en pantalla en números reales.
x,y: coordenadas reales del sprite en pantalla (transformadas de rx y ry).
iAncho, iAlto: ancho y alto del sprite en pixels.
bVisible: ¿Está visible en pantalla?
bTransparente: En un sprite transparente sólo son transparentes ciertas zonas del sprite.

Muchos os preguntaréis ¿Qué hace este tío guardando las coordenadas del sprite en formato flotante si en la pantalla se guardan las coordenadas como números enteros?. La explicación es sencilla: permite controlar la velocidad de movimiento con más precisión y es imprescindible cuando vamos a crear rutinas físicas para controlar la gravedad, la aceleración, etc.

Para solucionar el tema de que un sprite pueda tener muchas posiciones, se me ocurrió el crear subsprites dentro del sprite. Es como dividir un sprite en trozos horizontal y verticalmente:


Para ello utilizo estas variables dentro de la clase TSprite:

bSubsprites: ¿Tiene subsprites o es un sprite simple? Por defecto está a False.
iNumSubX, iNumSubY: Nº de subsprites horizontal y verticalmente.
iAnchoSub, iAltoSub: Ancho y alto máximo de cada uno de los subsprites.
iSubX, iSubY: subsprite seleccionado actualmente (columna y fila)
Superficie: Es el bitmap en memoria de video que contiene la imagen BMP cargada.

Vamos a ver la hora la implementación de la clase TSprite. Primero tenemos el constructor que inicializa todos los valores:

constructor TSprite.Create;
begin
Superficie := nil;
bVisible := True;
bTransparente := True;
rx := 0;
ry := 0;
x := 0;
y := 0;
iAncho := 0;
iAlto := 0;
bSubsprites := False;
iNumSubX := 0;
iNumSubY := 0;
iAnchoSub := 0;
iAltoSub := 0;
iSubX := 0;
iSubY := 0;
end;

Depués tenemos el desctructor que se encargar de eliminar la superficie (si esta cargada en memoria de vídeo):

destructor TSprite.Destroy;
begin
// Destruimos la superficie cargada
if Superficie <> nil then
begin
SDL_FreeSurface( Superficie );
Superficie := nil;
end;

inherited;
end;

Como las superficies son objetos especiales creados por la librería SDL no podemos llamar al método Free o FreeAndNil como si tal cosa. Hay que llamar a la función SDL_FreeSurface que se encarga de liberarla ya sea de la memoria de vídeo o de la memoria RAM.

Lo siguiente que necesitamos es la rutina que carga el sprite BMP del disco duro:

procedure TSprite.CargarSuperficie( sArchivo: string );
begin
Superficie := SDL_LoadBMP( PChar( sArchivo ) );

if Superficie = nil then
begin
ShowMessage( 'No se encuentra el archivo "' + sArchivo + '.' );
Exit;
end;

iAncho := Superficie.w;
iAlto := Superficie.h;

// Fijamos el negro como color transparente
if bTransparente then
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 0 );
end;

Lo que hace el método CargarSuperficie es cargar el bitmap del archivo y le asigna un color transparente si hemos puesto anteriormente la variable bTransparente a True.

También necesitamos un par de métodos para poder convertir en tiempo real un sprite a transparente o para hacerlo opaco. Un caso donde se suele dar es en los juegos de rol. En pantalla el héroe recoge el objeto transparente (por ejemplo una espada) y después la dibujamos en los marcadores sin que sea transparente. Para no tener que crear dos gráficos iguales (uno transparente y otro opaco) he creado estos dos métodos:

procedure TSprite.NoTransparente;
begin
// Lo hacemos opaco
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 255 );
end;

La función SDL_SetColorkey se encarga de decirle a una superficie cual va a ser el color transparente. Viene definida en la librería SDL del siguiente modo:

function SDL_SetColorKey(surface: PSDL_Surface; flag, key: UInt32) : Integer;

Sus parámetros son los siguientes:

Surface: superficie que queremos hacer transparente u opaca.
flag: nivel de transparencia (permite también invertir colores y hacer mezclas).
Key: Color que va a tomarse como transparente (si ponemos 255 es opaco).

procedure TSprite.Transparente;
begin
// Fijamos el negro como color transparente
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 0 );
end;

Yo he elegido el color negro como transparente pero puede ser cualquier otro. Se suele utilizar también mucho el color rosa ya que no es utilizado generalmente en los gráficos.

Y ahora viene el procedimiento más importante de todos:

procedure TSprite.Dibujar;
var
Origen, Destino: TSDL_Rect;
begin
// ¿Está visible el sprite?
if bVisible then
// ¿Tiene subsprites?
if bSubSprites then
begin
Origen.x := iSubX * iAnchoSub;
Origen.y := iSubY * iAltoSub;
Origen.w := iAnchoSub;
Origen.h := iAltoSub;
Destino.x := x;
Destino.y := y;
Destino.w := iAnchoSub;
Destino.h := iAltoSub;

// Dibujamos el subsprite seleccionado
if SDL_BlitSurface( Superficie, @Origen, Pantalla, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end
else
begin
Origen.x := 0;
Origen.y := 0;
Origen.w := iAncho;
Origen.h := iAlto;
Destino.x := x;
Destino.y := y;
Destino.w := iAncho;
Destino.h := iAlto;

// Lo dibujamos entero
if SDL_BlitSurface( Superficie, @Origen, Pantalla, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end;
end;

El procedimiento lo he dividido en dos partes principales. Si el sprite tiene subsprites entonces sólo capturo el trozo que me interesa y lo dibujo. En caso contrario dibujo todo el sprite.

Para dibujar un sprite con la librería SDL utilizo la función SDL_BlitSurface. Esta función viene definida en la librería SDL así:

function SDL_BlitSurface(src: PSDL_Surface; srcrect: PSDL_Rect; dst: PSDL_Surface; dstrect: PSDL_Rect): Integer;

Estos son sus parámetros:

Src: superficie origen.
srcrect: coordenadas del rectángulo origen.
dst: superficie destino.
dstrect: coordenadas del rectángulo destino.

PSDL_Rect es realmente un puntero a una estructura de datos (record en Delphi) definida de este modo:

TSDL_Rect = record
x, y: SInt16;
w, h: UInt16;
end;

Si tuviera que buscar una equivalencia entre dibujar con la librería SDL y con el GDI de Windows sería la siguiente:

Canvas = Superficie
TRect = TSDL_Rect

Así de simple y así de fácil.

PASAMOS A LA ACCIÓN

El juego lo vamos crear en una unidad aparte para no estropear nuestras rutinas estándar situadas en Juego.dpr y Juego.pas. Hay que pensar siempre en que si tengo que empezar un juego nuevo, copio estas dos unidades y listo.

Creamos una unidad nueva que se llame UArcade.pas y le vamos a vincular nuestra unidad estándar:

uses SysUtils, UJuego;

También voy a definir una variable global para guardar el gráfico de este avión:


Lo definimos en la sección interface:

var
Avion: TSprite;

El primer procedimiento que vamos a crear para esta unidad va a encargarse de cargar los sprites:

procedure CargarSprites;
begin
Avion := TSprite.Create;
Avion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'avion.bmp' );
end;

Doy por supuesto que el archivo avion.bmp se encuentra al lado de nuestro ejecutable. Este sprite no tiene subsprites, por lo que no hay que hacer nada en especial con el mismo.

También necesitamos otro procedimiento para eliminar los sprites de todo el juego:

procedure DestruirSprites;
begin
Avion.Free;
end;

Y otro para dibujarlos:

procedure DibujarSprites;
begin
Avion.Dibujar;
end;

MODIFICANDO EL BUCLE PRINCIPAL DE JUEGO

Después de toda la parafernalia que he montado vamos a ver los resultados reales. Para ello necesitamos llamar a estos nuevos procedimientos desde el bucle principal del programa situado en la unidad Juego.dpr:

program juego;

uses
Windows,
Dialogs,
SysUtils,
UJuego in 'UJuego.pas',
UArcade in 'UArcade.pas';

{$R *.res}

begin
InicializarSDL;
ModoVideo( 640, 480, 16, True );
Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;
CargarSprites;

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
DibujarSprites;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

DestruirSprites;
Temporizador.Free;
Teclado.Free;
FinalizarSDL;
end.

Si os fijáis en la sección uses he añadido la nueva unidad UArcade.pas.

Todo el bucle está igual que el artículo anterior anterior con la salvedad de que he llamado a los procedimientos CargarSprites, DibujarSprites y DestruirSprites.

Al ejecutar el juego tiene que aparecer el avión en la esquina superior izquierda de la ventana:


En el siguiente artículo veremos como mover el sprite por la pantalla incluso con un fondo. También veremos como moverlo utilizando el teclado y el ratón.


Pruebas realizadas en Delphi 7.

02 mayo 2008

Programar videojuegos con la librería SDL (2)

Antes de continuar desarrollando nuestro juego necesitamos bajarnos de Internet la librería SDL.DLL donde realmente están las rutinas SDL. Hay que tener en cuenta que la librería SDL está desarrollada en C y que el projecto JEDI-SDL contiene sólo las cabeceras de la librería en pascal.

Aquí tenéis el enlace directo para descargarse los archivos DLL que necesitamos:

http://www.libsdl.org/release/SDL-1.2.13-win32.zip

Este archivo hay que descomprimirlo al lado del ejecutable de nuestro juego (juego.exe). Se puede poner también dentro del directorio C:\Windows\System32\ pero es mejor dejarlo en nuestro directorio del proyecto, así hacemos el juego más portable pudiendo incluso llevarlo en un llavero USB.

Ahora vamos a seguir añadiendo rutinas genéricas a nuestro juego.

LEER EL TECLADO CON LA LIBRERÍA SDL

Otra cosa básica que necesita un juego mientras se está desarrollando es poder salir en cualquier momento del mismo con la tecla ESCAPE. Para controlar los eventos de teclado vamos a crear dentro de la unidad UJuego.pas una clase llamada TTeclado para encapsular todo lo referente respecto a este dispositivo.

Primero declaramos en la interfaz de la unidad la clase TTeclado:

type
TTeclado = class
bArriba, bAbajo, bDerecha, bIzquierda: Boolean; // Direcciones del cursor
bEspacio, bIntro, bCtrl_Izq: Boolean; // Teclas principales
Evento: TSDL_Event; // Clase evento de la librería SDL
iUltTec: Integer; // Última tecla pulsada

procedure Leer;
end;

He creado una variable booleana por cada tecla pulsada. Para un juego las teclas principales son los cursores del teclado, la tecla espacio, la tecla intro y la tecla Control (en la librería SDL se pueden leer indistintamente las teclas CTRL derecho e izquierdo). También he creado una variable llamada Evento de la clase TSDL_Event encargada de recoger las teclas pulsadas. Y por último, la variable iUltTec es el código de la última tecla pulsada por el usuario que nos será de utilidad más adelante.

Para capturar los eventos del teclado he creado el método Leer :

procedure TTeclado.Leer;
begin
// ¿Se ha producido un evento?
if SDL_PollEvent( @Evento ) > 0 then
begin
// ¿Ha pulsado una tecla?
if Evento.type_ = SDL_KEYDOWN then
begin
iUltTec := Evento.key.keysym.sym;

case Evento.key.keysym.sym of
SDLK_ESCAPE: bSalir := True;
SDLK_RIGHT: bDerecha := True;
SDLK_LEFT: bIzquierda := True;
SDLK_UP: bArriba := True;
SDLK_DOWN: bAbajo := True;
SDLK_SPACE: bEspacio := True;
SDLK_RETURN: bIntro := True;
SDLK_LCTRL: bCtrl_Izq := True;
end;
end;

// ¿Ha levantado una tecla?
if Evento.type_ = SDL_KEYUP then
begin
iUltTec := 0;

case Evento.key.keysym.sym of
SDLK_RIGHT: bDerecha := False;
SDLK_LEFT: bIzquierda := False;
SDLK_UP: bArriba := False;
SDLK_DOWN: bAbajo := False;
SDLK_SPACE: bEspacio := False;
SDLK_RETURN: bIntro := False;
SDLK_LCTRL: bCtrl_Izq := False;
end;
end;
end;
end;

Para leer el teclado realizamos una llamada a la función SDL_PollEvent que viene definida de esta manera:

function SDL_PollEvent( event: PSDL_Event ): Integer;

Lo que hace es leer un evento cualquiera y lo deposita en la variable event que le pasamos como parámetro. Al igual que ocurre con los eventos de teclado en la librería VCL de Delphi, en la librería SDL se distingue cuando se pulsa una tecla y cuando se suelta.

En un videojuego esto es muy importante ya que cada PC trabaja a una velocidad diferente dependiendo del procesador, la memoria RAM, la tarjeta gráfica, etc. Los videojuegos son muy sensibles a estos cambios ya que nuestros algoritmos se ejecutan entre 25 y 30 veces por segundo.

Si en un juego tipo matamarcianos el usuario deja pulsada la tecla de disparo, las ráfagas que salen de nave deben ir a la misma velocidad indistintamente del hardware que tenga nuestro PC . Y os puedo asegurar con rotundidad que la lectura de teclado de un PC a otro cambia una barbaridad (según teclado: Genius, Logitetch, etc.). Por tanto utilizo variables lógicas para saber en todo momento cuando esta pulsada una tecla y cuando no.

Para ello he utilizado la variable type_ que lleva el objeto Evento:

// ¿Ha pulsado una tecla?
if Evento.type_ = SDL_KEYDOWN then
...

// ¿Ha levantado una tecla?
if Evento.type_ = SDL_KEYUP then
...

Después leo la tecla pulsada según su código:

case Evento.key.keysym.sym of
SDLK_RIGHT: bDerecha := False;
SDLK_LEFT: bIzquierda := False;
.....

Las definiciones de todas las teclas de la librería están definidas dentro de la unidad SDL.pas siendo estas las más importantes para un videojuego las siguientes:

SDLK_RIGHT -> Cursor derecho
SDLK_LEFT -> Cursor izquierdo
SDLK_UP -> Cursor arriba
SDLK_DOWN -> Cursor abajo
SDLK_SPACE -> Espacio
SDLK_RETURN -> Intro
SDLK_LCTRL -> CTRL izquierdo
SDLK_RCTRL -> CTRL derecho
SDLK_LSHIFT -> Mayúsculas izquierdo
SDLK_RSHIFT -> Mayúsculas derecho
SDLK_LALT -> ALT izquierdo
SDLK_RALT -> ALT derecho
SDLK_F1 -> Tecla de función F1

SDLK_F12 -> Tecla de función F12
SDLK_INSERT -> Insertar
SDLK_HOME -> Inicio
SDLK_END -> Fin
SDLK_PAGEUP -> Avanzar página
SDLK_PAGEDOWN -> Retroceder página
SDLK_a -> Tecla A

SDLK_z -> Tecla Z


COMO CONTROLAR EL TIEMPO EN DISTINTOS ORDENADORES

Otro de los problemas que suele darse en un videojuego es que las figuras gráficas (sprites) suelen moverse a distinta velocidad según la tarjeta de video, por lo tanto debemos provocar ralentizaciones a propósito para que el juego funcione a 25 frames por segundo. Para conseguir esto vamos a crear una clase llamada TTemporizador:

TTemporizador = class
iTmpActual, iTmpUltimo: Integer;

constructor Create;
procedure Actualizar;
procedure Incrementar;
function Activado: Boolean;
procedure Esperar;
end;

Las variables iTmpActual y iTmpUltimo las voy a utilizar para que el bucle principal del juego se ejecute exactamente cada 25 milisegundos. En el constructor de la clase inicializo los temporizadores:

constructor TTemporizador.Create;
begin
iTmpUltimo := SDL_GetTicks;
end;

El método Actualizar va a encargarse de refrescar el temporizador:

procedure TTemporizador.Actualizar;
begin
iTmpActual := SDL_GetTicks;
end;

Con el método Incrementar controlo el tiempo que han consumido nuestras rutinas:

procedure TTemporizador.Incrementar;
begin
iTmpUltimo := iTmpUltimo + iTmpActual - iTmpUltimo;
end;

El método Activado nos dice cuado podemos ejecutar nuestras rutinas:

function TTemporizador.Activado: Boolean;
begin
Result := iTmpActual - iTmpUltimo >= 25;
end;

Y método Esperar le deja el control del procesador a Windows cuando no estemos en nuestros 25 milisegundos de proceso:

procedure TTemporizador.Esperar;
begin
SDL_Delay( 25 - ( iTmpActual - iTmpUltimo ) );
end;

La función SDL_Delay hace que nuestro juego se espere el número de milisegundos que le pasamos como parámetro. Lo que hace es devolverle el control a Windows para que baje el consumo de procesador. Equivale a provocar el evento OnIdle en Delphi.

También vamos a añadir un procedimiento global para actualizar la pantalla de video muestras se está ejecutando el juego:

procedure ActualizarPantalla;
begin
SDL_Flip( Pantalla );
end;

Este procedimiento refresca la pantalla de video mediante la técnica de doble buffer que veremos más adelante.

CREANDO EL BUCLE PRINCIPAL DEL JUEGO

Dentro de la unidad Juego.dpr vamos a inicializar el juego y mantener su control hasta el usuario pulse la tecla ESCAPE:

program juego;

uses
Windows, Dialogs, SysUtils, UJuego;

{$R *.res}

begin
InicializarSDL;
ModoVideo( 640, 480, 16, True );
Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

Temporizador.Free;
Teclado.Free;
FinalizarSDL;
end.

Como puede apreciarse en el código fuente ni siquiera hemos utilizado el objeto Application para inicializar el juego. El juego corre de manera pura en Win32 sin utilizar las librerías ni los objetos estándar de Delphi. De este modo, el juego funciona en ensamblador puro a toda su potencia tal como si estuviera programado en Visual C/C++ de Microsoft, tal como están hechos la mayoría de los videojuegos profesionales.

Vamos a ver parte por parte que de está compuesto el módulo principal del juego.

Vinculamos las unidades estándar de Windows y por supuesto nuestra unidad especial UJuego.pas.

uses
Windows, Dialogs, SysUtils, UJuego;

Inicializamos la librería SDL y creamos el juego a una resolución de 640 por 480 pixels con 16 bits de color por píxel (65535 colores) y en modo ventana:

InicializarSDL;
ModoVideo( 640, 480, 16, True );

Creamos los objetos que van a encargarse de controlar el teclado y el tiempo:

Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;

Este es el bucle principal de la aplicación que va a ejecutarse 25 veces por segundo:

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

Mientras que el usuario no pulse la tecla ESCAPE la variable bSalir estará a False. Después actualiza el temporizador y cada 25 milisegundos lee el teclado, actualiza la pantalla y incrementa el contador de milisegundos. Cuando haya pasado nuestro intervalo de 25 milisegundos de ejecución devolvemos a Windows el control mediante el método Esperar del objeto Temporizador.

Cuando el usuario pulse ESCAPE liberamos los objetos creados y finalizamos la librería SDL:

Temporizador.Free;
Teclado.Free;
FinalizarSDL;

Al ejecutar el juego debe salir una pantalla de 640 por 480 con el fondo negro esperando a que el usuario pulse ESCAPE:

Mientras el juego está en ejecución el consumo de procesador por parte del mismo debe ser mínimo:



Una vez tenemos desarrollada la parte más importante del juego (el núcleo central) ya podemos pasar a crear en juego en cuestión.

Eso tocará en el siguiente artículo.

Pruebas realizadas en Delphi 7.

25 abril 2008

Programar videojuegos con la librería SDL (1)

Hace mucho tiempo leí en una página web que uno no se hace programador profesional hasta que ha programado uno o más videojuegos (o por lo menos lo ha intentado). Por mi experiencia personal creo que tienen razón. En un videojuego no hay trucos de lenguajes, librerías o componentes. Todo hay que hacerlo a pelo. Nuestras funciones y procedimientos se ejecutan de 25 a 30 veces por segundo y ante cualquier error nuestro (bucles cerrados, objetos creados y no liberados, etc.) nos podemos cargar la pila de nuestra aplicación o el heap de datos mejor que el virus más maléfico del mercado.

Peor era en mis tiempos, cuando programaba en ensamblador con mi MSX que tenía un procesador Z80 de 8 bits. Cuando se colgaba el juego había que resetear la máquina, rebobinar la cinta de casete y esperar dos minutos a que se cargue el ensamblador y el debugger. Eso si que eran buenos tiempos.

Aunque no os preocupéis, la programación de videojuegos en PC no es tan traumática. Vamos a ver paso a paso como crear un nuevo proyecto en Delphi y adaptarlo para añadirle la librería SDL.

SDL (Simple DirecMedia Layer) es una librería multiplataforma escrita en el lenguaje de programación C/C++ y disponible para todos los sistemas operativos (Windows, Linux, BeOS, NetBSD, etc.). Permite controlar todo lo referente al video 2D y 3D con OpenGL, reproduce archivos multimedia y controla el teclado, ratón o joystick.

DONDE CONSEGUIR LA LIBRERÍA SDL PARA DELPHI

Como todos sabéis, existe la página Project JEDI Portal encargada de traducir librerías y utilidades en C/C++ al Pascal/Delphi. Dentro de esa inmensa cantidad de librerías se encuentra el proyecto JEDI-SDL que es el que nos interesa:

Es en esta dirección donde está lo que nos interesa:

http://sourceforge.net/project/showfiles.php?group_id=43805

El archivo es un zip de 5.5 MB. Lo que tenemos que hacer es crear una carpeta donde alojar la librería de tal modo que varios proyectos puedan tirar de ella, por ejemplo:

D:\desarrollo\librerias\sdl\

Ahí es donde tenemos que descomprimir el archivo JEDI-SDLv1.0.zip que nos hemos descargado. Nos va a extraer todas estas carpetas:


Una vez tenemos nuestra librería instalada ya podemos ponernos manos a la obra.

CREANDO UN JUEGO DELPHI CON LA LIBRERÍA SDL

Los pasos para crear un nuevo juego serían los siguientes:

1. Creamos un nuevo proyecto y lo guardamos en la carpeta donde vamos a crear el juego, por ejemplo:

D:\desarrollo\delphi\juego\

2. Eliminamos el formulario principal Form1 (si, habéis leído bien, nos vamos a cargar todos los formularios del proyecto. Hay que olvidarse de formularios y componentes).

3. Guardamos el proyecto con el nombre juego.dpr

4. Abrimos la única unidad que tiene el proyecto: CTRL + F12 y Juego.

Para que el código fuente quede lo más limpio posible sólo vamos a insertar en la unidad juego.dpr el núcleo central del programa. Todo lo referente a las rutinas de inicialización y manejo de la SDL la vamos a añadir en otra unidad.

CREANDO UNA UNIDAD PARA RUTINAS GENÉRICAS

Vamos a crear la unidad UJuego.pas la cual va a contener todas las rutinas genéricas que puede tener un juego. Cuando se diseña un programa siempre hay que pensar en reutilizar el código para un futuro.

Lo primero que hay que añadir en esta unidad es una referencia a la librería SDL. Para ello primero vamos a vincular al proyecto las unidades SDL que necesitamos. En el menú superior de Delphi seleccionamos Proyect -> Options y en la pestaña Directories/Conditionals vamos a añadir al campo Search Path la carpeta:

D:\Desarrollo\Librerias\sdl\SDL\Pas\

De este modo ya podemos vincular la librería SDL a nuestra unidad:

También he añadido las unidades Windows, SysUtils y Dialogs porque las vamos a necesitar para llamar a algunas funciones y procedimientos estándar.

INICIALIZAR LA LIBRERÍA SDL

El primer procedimiento que vamos a crear se va a llamar InicializarSDL, el cual va a encargarse de inicializar el modo de video en 2D para poder dibujar:

procedure InicializarSDL;
begin
// Inicializamos la librería SDL
if SDL_Init( SDL_INIT_VIDEO or SDL_DOUBLEBUF ) < 0 then
begin
ShowMessage( 'Error al inicializar la librería SDL.' );
SDL_Quit;
bSalir := True;
Exit;
end;

bSalir := False;
end;

bSalir es una variable global que vamos a declarar también en la unidad UJuego.pas para controlar si hay que salir del juego inmediatamente:

Var
bSalir: Boolean;

La función SDL_Init se encarga inicializar la librería SDL, controlando todos los dispositivos que vamos a inicializar: video, sonido, ratón, joysticks, etc. Tiene los siguientes parámetros:

function SDL_Init( flags : UInt32 ) : Integer;

donde flags puede contener a la vez todos estos valores:

SDL_INIT_TIMER: Prepara el temporizador de la librería SDL.
SDL_INIT_AUDIO: Prepara la tarjeta de sonido.
SDL_INIT_VIDEO: Prepara el modo de video.
SDL_INIT_CDROM: Prepara el CD-ROM.
SDL_INIT_JOYSTICK: Inicializa el joystick.
SDL_INIT_EVERYTHING: Inicializa todos lo que hemos visto anteriormente.

También vamos a crear otro procedimiento para finalizar la ejecución de la librería SDL:

procedure FinalizarSDL;
begin
SDL_Quit;
end;

Este procedimiento es importante para que la librería SDL suelte los dispositivos que tiene cogidos en Windows (teclado, ratón, tarjeta de sonido, etc.).

CAMBIAR EL MODO DE VÍDEO EN SDL

El siguiente procedimiento que vamos a crear se va a encargar de cambiar el modo de video:

procedure ModoVideo( iAncho, iAlto, iProfundidadColor: Integer; bModoVentana: Boolean );
begin
// Pasamos a modo de video de la ventana especificada
if bModoVentana then
Pantalla := SDL_SetVideoMode( iAncho, iAlto, iProfundidadColor, SDL_HWSURFACE )
else
Pantalla := SDL_SetVideoMode( iAncho, iAlto, iProfundidadColor, SDL_HWSURFACE
or SDL_FULLSCREEN );

if Pantalla = nil then
begin
ShowMessage( 'Error al cambiar de modo de video.' );
SDL_Quit;
Exit;
end;
end;

Los parámetros son los siguientes:

iAncho -> Resolución horizontal en pixels (320, 640, 800, 1024)
iAlto -> Resolución vertical en pixels (200, 480, 600, 768)
iProfundidadColor -> número de bits por píxel (8, 16, 32)
bModoVentana -> True: en ventana False: Pantalla completa

Cuando se desarrolla un juego por primera vez lo mejor es hacerlo en modo ventana, porque si se produce un error va a costar mucho volver al escritorio de Windows e incluso puede dejarnos el modo de video en una resolución inferior a la que deseamos (desordenando todos los iconos del escritorio). Cuando el juego esté terminado y funcione bien entonces activamos el modo a pantalla completa.

La resolución de los modos de video estándar son los siguientes:

320 x 200
640 x 480
800 x 600
1024 x 768

La profundidad de color se refiere al número de bits por pixel pudiendo seleccionar los valores: 8, 16 o 32. Estos son los valores estándar en la creación de videojuegos, pero hay muchos modos de vídeo más según la tarjeta (nVidia, ATI, Matrox, Intel, etc.). Si vamos a hacer un juego normal que no consuma muchos recursos tenemos dos opciones:

ModoVideo( 640, 480, 16, False );
ModoVideo( 800, 600, 16, False );

Si la tarjeta de video es buena (una aceleradora 3D) los valores serían:

ModoVideo( 640, 480, 32, False );
ModoVideo( 800, 600, 32, False );

Estas son las resoluciones que más se utilizan en el desarrollo de juegos Indie (los juegos Indie son los realizados por desarrolladores independientes pero que suelen tener buena calidad y se venden a bajo precio: de 10 a 20 €).

La variable Pantalla que hemos utilizado en el procedimiento ModoVideo la vamos a declarar también como global (al lado de bSalir):

var
bSalir: Boolean;
Pantalla: PSDL_Surface;

PSDL_Surface es el puntero a un objeto SDL_Surface. Para poder dibujar en SDL se necesita una superficie, la cual puede estar en la memoria de video (lo recomendable) o en la memoria RAM (sólo para guardar sorites y texturas cuando son muchas).

El puntero Pantalla que yo he creado representa directamente la pantalla de video donde vamos a dibujar. Para crear esta pantalla he utilizado la siguiente función de la librería SDL:

function SDL_SetVideoMode(width, height, bpp: Integer; flags: UInt32): PSDL_Surface;

Los parámetros son los siguientes:

Width -> Ancho de la superficie
Height -> Altura de la superficie
Bpp -> Número de bits por píxel
Flags -> Que tipo de superficie vamos a crear

Los tipos de superficie que admite el parámetro flags son los siguientes:

SDL_SWSURFACE: la superficie estará en la memoria RAM
SDL_HWSURFACE: la superficie estará en la memoria de video (recomendado)

También se puede combinar con los siguientes valores:

SDL_ANYFORMAT: admite cualquier resolución o profundidad de color
SDL_HWPALETTE: tendrá una paleta de color exclusiva (sólo se utiliza en modos de 8 bits, esto ya está obsoleto).
SDL_DOUBLEBUF: tiene doble buffer (ya lo explicaré más adelante)
SDL_FULLSCREEN: la superficie estará a pantalla completa
SDL_OPENGL: va a utilizar la libería OpenGL para dibujar polígonos en 3D
SDL_RESIZABLE: el modo de video puede ser redimensionado
SDL_NOFRAME: la ventana no tendrá ni título ni bordes

En la siguiente parte de este artículo crearemos el bucle principal de la aplicación y veremos como cargar las figuras gráficas (sprites) para darles movimiento.

Pruebas realizadas en Delphi 7.

18 abril 2008

Formateando código automáticamente con DelForExp

Esta es otra herramienta que con el tiempo se me ha ido haciendo imprescindible junto con Delphi. DelForExp es un experto (y gratuito) que se puede instalar en cualquier versión de Delphi y que formatea el código fuente automáticamente según nuestras preferencias.

Con esta utilidad ya no será necesario utilizar las combinaciones de teclas CTRL + K + I y CTRL + K + U para identar las líneas de código cada vez que copiamos un trozo de código de un lugar a otro. Otro de los “desastres” que suele solucionar es cuando se trabaja en equipo y cada programador escribe el código a su manera (los begin, end, los espacios, los paréntesis, etc.).

Para descargar esta interesante utilidad hay que irse a la siguiente dirección:

http://www.aew.wur.nl/UK/Delforexp/

La instalación va comprimida en un zip de 564 KB. Al no llevar realmente una instalación como Dios manda hay que descomprimirlo en alguna carpeta donde sepamos que no se va a borrar (ya que contiene librerías DLL que se van a enganchar a Delphi como lapas). Después ejecutamos el archivo SetupEx.exe y comenzará a instalarse en todos los Delphi que tengamos instalados:

Es una instalación algo cutre pero efectiva, ya que si abrimos Delphi y miramos en el menú superior la opción Tools aparecerá una opción nueva llamada Source Formatter:

Al ejecutar esta opción nos aparece esta ventana:

Mucho cuidado de pulsar los primeros tres botones (Current File, All Open Files y Whole Project) antes de establecer el tipo de formateo de texto. Antes de hacer experimentos recomiendo hacer una copia de seguridad de nuestro proyecto, sobre todo para evitar sustos.

ESTABLECIENDO EL FORMATO DE TEXTO

Para seleccionar nuestro propio estilo de programación debemos pulsar el botón Options:

En la primera pestaña (ident) establecemos los espacios que vamos a meter para identar nuestro código. Lo ideal es dejar el campo Number of spaces per ident a 2, que es lo estándar en Delphi. También es conveniente identar los comentarios mediante la opción Ident comments para que la alineación de nuestros comentarios no se quede descolocada respecto al código.

Igual se puede hacer con las directivas del compilador, los bloques de código después del comando else, etc. Lo mejor es dejarlo como se ve en la foto que he mostrado, con los comentarios es suficiente.

En la siguiente pestaña (Spacing) le indicamos cuantos espacios queremos alrededor de los símbolos (paréntesis, comas, etc.):

A mí por ejemplo me gusta que deje un espacio después de abrir un paréntesis y otro antes de cerrarlo:

procedure TForm1.FormCreate( Sender: TObject );

Para ello debemos seleccionar en la línea Left Parenthesis el valor Alter only (sólo después) y en la línea Right Parenthesis el valor Befote Only (sólo antes). También se puede hacer lo mismo con los corchetes.

En la tercera pestaña (Line breaks) le podemos indicar cuando debe hacer un Intro para pasar a la línea siguiente. En el siguiente ejemplo lo he puesto para procedimientos y funciones:

La cuarta pestaña (Capitalization) se utiliza para pasar a mayúsculas la primera letra de las palabras reservadas o comandos:

Por defecto, están ambas opciones desactivadas. Si activamos la opción Reserved words pasará a mayúsculas la primera letra de todas la palabras reservadas (While, Begin, etc.).

La quinta pestaña (Align) se utiliza para alinear los comentarios a una línea que solemos poner detrás de nuestro código para dejarlos alineados:

Por ejemplo, en este bloque de código hay comentarios detrás de cada línea:

procedure TFPrincipal.Inicializar( Sender: TObject; var Done: Boolean );
begin
Done := True; // El proceso se ha realizado correctamente
Application.OnIdle := nil; // Liberamos el uso de la aplicación
Liberar_Memoria; // Liberamos memoria
Bloquear_Ventana( True ); // Impedimos que usuario modifique los datos
end;

Como puede verse los comentarios quedan sin alinear respecto al código. Para corregir ese problema activo la opción Aling simple comments alter code con el valor 40 y así quedaría al formatear el código:

procedure TFPrincipal.Inicializar( Sender: TObject; var Done: Boolean );
begin
Done := True; // El proceso se ha realizado correctamente
Application.OnIdle := nil; // Liberamos el uso de la aplicación
Liberar_Memoria; // Liberamos memoria
Bloquear_Ventana( True ); // Impedimos que usuario modifique los datos
end;

Como puede verse el código queda mucho más elegante y nos evita machacar la barra de espacio o el tabulador.

La sexta pestaña (Misc.) se utiliza para dar formateo a las directivas del compilador. También podemos establecer qué combinación de teclas vamos a utilizar para formatear el código fuente de la unidad que estamos tocando en ese momento (que por defecto es la combinación de teclas CTRL + D):

Y en la última pestaña (Preview) podemos ir viendo en tiempo real cómo quedaría el código fuente según apliquemos unas opciones u otras:

También podemos pulsar el botón Borland Style para que lo deje al estilo clásico de Borland (por si la hemos fastidiado con alguna opción y no sabemos dejarlo como estaba).

Después de haber establecido nuestro estilo de código, ahora podemos aplicarlo pulsando los botones:

Current File -> Formatea el código fuente de la unidad donde estamos situados.

All Open Files -> Formatea el código de todas las pestañas de código que tengamos abiertas en el proyecto.

Whole Proyect -> Formatea el código de todas las unidades del proyecto.

Como he dicho anteriormente, antes de pulsar los botones All Open files o Whole Proyect sería aconsejable hacer copia de seguridad del proyecto (o por lo menos de los archivos .pas).

De todas formas, llevo más de una año utilizando esta herramienta y jamás me ha jodido el código fuente o me ha provocado ningún problema. Es fiable 100%.

Cuando estemos escribiendo código fuente y queramos formatear el código sólo hay que pulsar la combinación de teclas CTRL + D (aparecerá la ventana de DelForExp) y luego la tecla Intro (ya que estará enfocado el botón Current File).

Con herramientas como esta se ahorra mucho tiempo y además se puede compartir el código fuente con otros usuarios sin que hayan quejas sobre los estilos de escritura de cada programador.

Pruebas realizadas en Delphi 7.

11 abril 2008

Cazando errores con EurekaLog

Cuando uno encuentra herramientas como esta, a veces le dan ganas de darse cabezazos contra la pared después de haber sufrido los castigos del debugger de Delphi. EurekaLog es un experto que se acopla perfectamente a cualquier versión de Delphi y que permite capturar excepciones, access violation y pérdidas de memoria todo ello en una sóla herramienta (y además sin modificar para nada las opciones de Delphi o de nuestro proyecto).

Funciona incluso con las últimas versiones de Delphi (Delphi for Win32 y RadStudio 2007). Para versiones muy antiguas de Delphi, como por ejemplo Delphi 5 a veces requiere tener instalado algún Update que otro.

Aunque sea un programa comercial podemos bajar la demo de su página oficial:

http://www.eurekalog.com/downloads.php

La última versión a fecha de este artículo es la 6.0.12, teniendo el archivo de instalación un tamaño de 12.4 MB. Antes de ejecutar la instalación es conveniente tener todos los Delphi cerrados. La instalación no tiene muchas complicaciones:

Vamos pulsado el botón Next hasta que finalice la instalación. En uno de estos pasos reconoce las distintas versiones de Delphi que tenemos instaladas, permitiendo seleccionar las que nos interesen:

Cuando termina la instalación nos da la posibilidad de visualizar un videotutorial online sobre cómo funciona EurekaLog, lo cual facilita mucho las cosas.

Ahora arrancamos Delphi y abrimos un proyecto cualquiera para probarlo. Para activar EurekaLog en nuestro proyecto hay que ejecutar las opciones Proyect -> Eureka Options:

En la ventana que aparece debemos activar la opción Activate EurekaLog. Esto hará que intercepte los errores en tiempo de ejecución sin necesidad de tener activada la opción Integrated Debugging del propio Delphi. Si también queremos interceptar los errores de pérdida de memoria debemos irnos a la sección llamada Avanced Options y seleccionar la opción Catch Memory Leaks:

Después pulsamos el botón Ok y ya tenemos EurekaLog listo para cazar bichos. Vamos a verlo con unos ejemplos.

CAPTURANDO ACCESS VIOLATIONS

Uno de los errores que más solemos cometer es el utilizar objetos que no hemos creado. Por ejemplo, voy a crear un formulario con un botón en el cual si hago clic intentaré añadir un elemento a un objeto StringList sin haberlo creado anteriormente:

procedure TForm1.Button1Click( Sender: TObject );
var S: TStringList;
begin
S.Add( 'añadiendo elemento' );
end;

Veamos como se comporta EurekaLog en este caso. Al hacer clic sobre el botón nos aparece esta ventana en vez de la típica de Access Violation:

Si pulsamos la opción click here nos mostrará esta otra ventana:

En la primera pestaña (General) nos informa de la excepción que ha provocado la aplicación así como todas las características del equipo donde se está ejecutando. Esta información es muy interesante para averiguar en que máquina esta corriendo nuestra aplicación.

En la segunda pestaña (Call Stack) podemos ver en que línea de código se ha producido el error:

Y ahora viene lo mejor de todo. Si hacemos doble clic sobre la línea azul saltará directamente a Delphi y se irá a la línea de código donde se ha producido el error. Vosotros os preguntareis ahora, ¿y que tiene de especial si el depurador de Delphi ya lo hace? Pues porque EurekaLog caza errores donde Delphi no llega. ¿Cuántos access violations os ha llevado Delphi al depurador en ensamblador? ¿Cuántas veces os ha explotado Delphi sin tener ni idea donde se ha producido el error? A mi por lo menos se me ha quedado el puntero del depurador en Application.Run y se ha quedado como Dios.

Con EurekaLog he llegado a cazar el 95% de errores de Delphi donde el debugger no llegaba. Además me sitúa el cursor exactamente en la línea de código donde se ha producido el error.

En la tercera pestaña (Modules) podemos ver nuestro ejecutable y cuantas librerías DLL hay cargadas en memoria:

En la cuarta pestaña (Processes) se ve los ejecutables que estaban corriendo en el sistema en el momento del error:

En la quinta pestaña (Assembler) nos muestra el punto de ejecución en ensamblador donde se ha quedado el procesador:

Y en la última pestaña (CPU) tenemos un volcado de los registros del procesador, la pila y la memoria del segmento actual:

Si pulsamos el botón Ok, la aplicación seguirá su curso y devolverá el control a Delphi.

AVERIGUANDO DONDE SE PRODUCEN LAS PERDIDAS DE MEMORIA

Este es otro de los agujeros negros que sufrimos los programadores de Delphi, las pérdidas de memoria (Memory Leaks). Yo hasta ahora había utilizado la unidad MemCheck.pas para cazar los errores, pero es muy incómoda su instalación y seguimiento. En este caso EurekaLog también es insuperable, cazando las perdidas de memoria al vuelo. Veamos otro ejemplo.

Voy a crear un StringList y a salirme del programa sin hacer Free del objeto:

procedure TForm1.Button1Click( Sender: TObject );
var S: TStringList;
begin
S := TStringList.Create;
S.Add( 'añadiendo elemento' );
end;

Al finalizar el programa saltará la siguiente ventana de EurekaLog:

Cuando pulsemos sobre la opción click here y nos vayamos a la pestaña Call Stack nos dirá exactamente que línea ha creado algo en memoria y no lo ha liberado:

Haciendo doble clic sobre la misma saltará a la línea de código responsable del problema:

S := TStringList.Create;

Esto deja a Delphi 2007 en ridículo en cuestión de cazar pérdidas de memoria, ya que este último IDE de CodeGear no dice ni en que unidad ni en que línea se ha producido la perdida de memoria, sólo nos dice que objetos han quedado sin liberar. Yo no se vosotros, pero para mi, eso y nada es lo mismo.

ENVIANDO LOS ERRORES POR CORREO ELECTRONICO

¿Cuántas veces os ha ocurrido que a un cliente vuestro le salta un Access Violation y a vosotros os funciona bien con la misma base de datos? Eso a mi me ha pasado un día si y otro también. Pues en ese caso enviamos el ejecutable de nuestra aplicación a nuestros clientes con EurekaLog activado y compilado, y en momento que se produzca la explosión a ellos también les aparecerá la misma ventana de EurekaLog con el error, con lo cual podrán decirnos en que unidad y línea de código se ha producido el error.

Pero no sólo eso, si no queremos complicarnos la vida, EurekaLog permite enviar por correo electrónico toda la información perteneciente al error que se ha producido. Para ello volvemos a la ventana de opciones de EurekaLog y nos situamos en la primera sección (Email & WebSend):

Seleccionamos la opción SMTP Client dentro del campo Send Mode y rellenamos el resto de campos con la cuenta de correo que vamos a utilizar para enviar los errores. Esa cuenta se puede utilizar para enviarse errores así misma, gastando pocos recursos de nuestro servidor SMTP.

Cuando a nuestro cliente le ocurra un error con nuestra aplicación le aparecerá esta ventana:

Cuando pulse el botón Send Error Report comenzará a enviar el error por correo electrónico con los datos de la cuenta que le hemos dicho (si os fijáis en la imagen se puede enviar a otro correo opcional):

Junto con el mensaje de correo llevará adjunto un archivo comprimido con zip llamado BugReport.zip. Este a su vez tiene dos archivos dentro:

Screenshot.png que contiene una imagen capturada de todo el escritorio de Windows cuando se produjo el error.

ProbandoEureka.elf (Igual que el nombre de nuestro ejecutable pero con extensión .elf) que al doble clic sobre el mismo y nos abrirá un visor con el error:

Es decir, nos manda la pantalla capturada (para saber que estaba haciendo el usuario antes de saltar el error) y el archivo de depuración con la línea de código que provocó el error.

Aunque EurekaLog sea una herramienta comercial (cuesta 99 €) sin duda merece la pena comprarla si eso va a mejorar la productividad y ahorrarnos muchos dolores de cabeza. En resumen, otra herramienta imprescindible para Delphi.

Pruebas realizadas en Delphi 7.

04 abril 2008

El componente ValueListEditor

Hace bastante tiempo mostré como guardar valores dobles (nombre=valor) dentro de una lista (StringList), concrétamente fue en este artículo:

El Objeto StringList (I)

Lo que vamos a ver ahora es cómo almacenar esos mismos valores pero utilizando el componente visual de la clase TValueListEditor, el cual está alojado en la pestaña Additional:

Al insertar este componente en un formulario se muestra de este modo:


La columna Key contendrá el nombre de los valores que deseamos insertar y la columna Value el valor asociado al nombre. Supongamos que quiero que el usuario pueda modificar los datos principales de un cliente. Lo primero que vamos a hacer el cambiar el título de las columnas a través de la propiedad TitleCaptions. Vamos a sustituir Key y Value por Campo y Valor, de manera que así quedaría:

Lo siguiente que toca es introducir los campos del cliente. Para ello utilizamos la propiedad Strings del componente e introducimos los nombres de los campos en la columna Key:

Para crear más de una línea tenemos que pulsar en el teclado la tecla del cursor hacia abajo (la verdad es que este editor de valores es algo chapucero). Una vez introducidos los valores y pulsado el botón Ok dejaría este resultado:

Al ejecutar el programa se puede apreciar que este componente funciona de manera similar al inspector de objetos de Delphi (Object inspector). La primera columna se halla en modo lectura dejando que el usuario sólo pueda modificar los valores. Pero tenemos que ir un paso más allá: hay que conseguir que dependiendo del campo, el usuario sólo pueda meter valores de cierto tipo.

ESTABLECIENDO MASCARAS PARA CADA VALOR

El problema radica en que no se puede establecer el tipo de valor que puede introducirse en un campo en tiempo de diseño utilizando el inspector de objetos. Hay que hacerlo en el evento OnCreate del formulario mediante código. Por ejemplo, vamos a hacer que en el campo ID sólo se puedan introducir valores numéricos:

procedure TFPrincipal.FormCreate( Sender: TObject );
var
PropNumerica: TItemProp;
begin
PropNumerica := TItemProp.Create( ValueListEditor );
PropNumerica.EditMask := '###0';
ValueListEditor.ItemProps[0] := PropNumerica;
end;

Lo que hemos hecho es crear el objeto PropNumerica de la clase TItemProp y posteriormente hemos establecido la regla de edición asignándola al primer elemento de la lista (el campo ID).

También podemos crear otro tipo de propiedad que permita escoger el valor sólo entre una lista predeterminada. Por ejemplo, voy a hacer que el campo ESTADO sólo pueda contener los estados PENDIENTE o PAGADO:

procedure TFPrincipal.FormCreate( Sender: TObject );
var
PropNumerica, PropEstado: TItemProp;
begin
PropNumerica := TItemProp.Create( ValueListEditor );
PropNumerica.EditMask := '###0';
ValueListEditor.ItemProps[0] := PropNumerica;

PropEstado := TItemProp.Create( ValueListEditor );
PropEstado.PickList.Add( 'PENDIENTE' );
PropEstado.PickList.Add( 'PAGADO' );
ValueListEditor.ItemProps[5] := PropEstado;
end;

Al ejecutar el programa el campo ESTADO quedaría del siguiente modo:

También podemos modificar la forma en cómo se guardan los valores dentro de la rejilla a través de la propiedad KeyOptions en el inspector de objetos:

Aunque todas estas opciones hacen que este componente sea bastante útil, se echan en falta algunas características tales como valores para selección de color, máscaras para valores numéricos en coma flotante y la selección de fuentes de texto.

Pruebas realizadas en Delphi 7.

Publicidad