25 septiembre 2009

Mi primer videojuego independiente (2)



Una vez que hemos visto por encima las dificultades más importantes que se me dieron con el editor, vamos a pasar a ver las partes más importantes del juego. Sobre todo voy a centrarme en el apartado gráfico, ya que lo que se refiere a joystick, sonido, etc. no cambia casi nada respecto lo que escribí en los anteriores artículos referentes a la SDL.

EL NÚCLEO DEL JUEGO

El núcleo principal del juego no difiere demasiado respecto a lo escribí anteriormente. Como no se utiliza en casi nada las librerías de Delphi lo que hay que hacer es crear un nuevo proyecto, eliminar el formulario principal y escribir este código directamente en el archivo DPR:

begin
InicializarSDL('Pequepon');
CargarOpciones;
ModoVideo(640, 480, 32, bPantallaCompleta);
Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;
Joystick := TJoystick.Create;
Raton := TRaton.Create;
ControlSonido := TControlSonido.Create;
InicializarJuego;

while not bSalir do
begin
Temporizador.Actualizar;

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

Demo.Free;
DestruirSprites;
FinalizarJuego;
Raton.Free;
Joystick.Free;
ControlSonido.Free;
Temporizador.Free;
Teclado.Free;
FinalizarSDL;
end.

Podemos dividir el núcleo en tres bloques principales:

Inicialización: cambiamos el modo de vídeo y creamos los objetos que necesitamos a lo largo del juego:

InicializarSDL('Pequepon');
CargarOpciones;
ModoVideo(640, 480, 32, bPantallaCompleta);
Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;
Joystick := TJoystick.Create;
Raton := TRaton.Create;
ControlSonido := TControlSonido.Create;
InicializarJuego;

La variable booleana bPantallaCompleta recoge de las opciones si estamos en modo ventana o en pantalla completa.

Bucle infinito principal: Por este bucle pasará todo el juego 40 veces por segundo. No terminará hasta que la variable booleana bSalir este activada:

while not bSalir do
begin
Temporizador.Actualizar;

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

Finalización y destrucción de objetos: Nos deshacemos de todos los objetos que hay en memoria y finalizamos el modo de video para volver al escritorio de Windows:

Demo.Free;
DestruirSprites;
FinalizarJuego;
Raton.Free;
Joystick.Free;
ControlSonido.Free;
Temporizador.Free;
Teclado.Free;
FinalizarSDL;

Comencemos viendo las rutinas de inicialización más importantes.

INICIALIZANDO LA LIBRERÍA SDL

Todas las rutinas de mi motor 2D las introduje dentro de una unidad llamada UJuego.pas. De este modo, si tenemos que hacer otro juego sólo hay que importar esta unidad y tenemos medio trabajo hecho.

Veamos como arrancar la librería SDL con el procedimiento InicializarSDL:

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

SDL_WM_SetCaption(PChar(sTitulo), nil);
bSalir := False;
sRuta := ExtractFilePath(ParamStr(0));

// Inicializamos los sprites
iNumSpr := 0;
for i := 1 to MAX_SPRITES do
Sprite[i] := nil;
end;

Probamos a cambiar el modo de vídeo activando el doble buffer y el joystick. Si funciona entonces le ponemos el título a la ventana (por si ejecutamos el juego en modo ventana), memorizamos en la variable sRuta donde esta nuestro ejecutable (para cargar luego los sprites) e inicializamos los sprites que están declarados en este array global:

var
Sprite: array[1..MAX_SPRITES] of TSprite;

El número máximo de sprites que fijé que pueden estar a la vez cargados fue de 50:

const
MAX_SPRITES = 50;

Aunque nunca llegué a gastarlos del todo. Ya veremos la clase TSprite más adelante.

CAMBIANDO EL MODO DE VÍDEO

Como vamos a trabajar con OpenGL, aquí ya hay cambios importantes respecto a la rutinas de SDL tradicionales:

procedure ModoVideo(iAncho, iAlto, iProfundidadColor: Integer; bPantallaCompleta: Boolean);
begin
SetEnvironmentVariable('SDL_VIDEO_CENTERED', '1');

SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 32);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

// Activamos la sincronización vertical
SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1);

// Pasamos a modo de video de la ventana especificada
if bPantallaCompleta then
Pantalla := SDL_SetVideoMode(iAncho, iAlto, 32, SDL_FULLSCREEN or SDL_OPENGL)
else
Pantalla := SDL_SetVideoMode(iAncho, iAlto, 32, SDL_OPENGL);

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

InicializarOpenGL(iAncho, iAlto);
end;

Vamos a analizar este procedimiento porque es la base de todo. Antes de hacer nada le decimos a la SDL que en el caso de que ejecutemos el juego en modo ventana me la centre en el escritorio. Esto lo hacemos creando una variable de entorno:

SetEnvironmentVariable('SDL_VIDEO_CENTERED', '1');

Después le decimos a la librería OpenGL que vamos a renderizar polígonos con 32 bits de color y que active el doble buffer:

SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 32);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

Y aquí tenemos la razón de porque pase todo el juego a OpenGL:

// Activamos la sincronización vertical
SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1);

Luego activamos el modo de vídeo en modo ventana o pantalla completa según lo que nos pasen como parámetro:

if bPantallaCompleta then
Pantalla := SDL_SetVideoMode(iAncho, iAlto, 32, SDL_FULLSCREEN or SDL_OPENGL)
else
Pantalla := SDL_SetVideoMode(iAncho, iAlto, 32, SDL_OPENGL);

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

La variable Pantalla es una superficie de SDL donde vamos a renderizar todos los polígonos:

var
Pantalla: PSDL_Surface; // Pantalla principal de vídeo

PREPARANDO LA ESCENA OPENGL

Al final del procedimiento ModoVideo llamamos a otro procedimiento llamado InicializarOpelGL que inicializa la escena donde vamos a dibujar los polígonos:

procedure InicializarOpenGL(iAncho, iAlto: Integer);
var
rRatio: Real;
begin
rRatio := CompToDouble(iAncho) / CompToDouble(iAlto);

// Suavizamos los márgenes de los polígonos
glShadeModel(GL_SMOOTH);

// Ocultamos las caras opuestas
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

// Ponemos el negro como color de fondo
glClearColor(0, 0, 0, 0);

// Configuramos el zbuffer
glClearDepth(1);

// Establecemos la perspectiva de visión
gluPerspective(60, rRatio, 1.0, 1024.0);

// Habilitamos el mapeado de texturas
glEnable(GL_TEXTURE_2D);

// Activamos el z-buffer
glEnable(GL_DEPTH_TEST);
glDepthMask(TRUE);

// Sólo se dibujarán aquellas caras cuyos vértices se hallen en sentido
// de las agujas del reloj
glFrontFace(GL_CW);

glViewport(0,0,640,480);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0,640,0,480,-100,100);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
end;

Lo primero que hacemos en este procedimiento es calcular el ratio de perspectiva de la cámara respecto a al ancho y alto de la superficie:

var
rRatio: Real;
begin
rRatio := CompToDouble(iAncho) / CompToDouble(iAlto);

Luego configuramos la impresión de polígonos para que suavice los bordes dentados que aparecen en los márgenes de cada polígono. Esto es importante porque entre esta función y el suavizado utilizando el canal alfa da un aspecto a los sprites de dibujos animados:

glShadeModel(GL_SMOOTH);


Para dar un toque de suavidad a los sprites también hemos dibujado el contorno de los mismos de color negro pero dando una pequeña semitransparencia para que se mezcle con el fondo. Aunque el juego esté solo a 640 x 480 parece que está a más resolución.

Ahora le decimos que vamos a ocultar los polígonos con las caras opuestas. Esto sobre todo tiene más sentido en juegos 3D:

glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

Le estamos diciendo que sólo queremos que imprima los polígonos que estén mirando a la cámara según el orden que demos a los vértices que veremos más adelante. Imaginaos un cubo en 3D:

Si os dais cuenta, sólo se ven tres caras a la vez. Las otras tres caras siempre quedan ocultas. Entonces, ¿para que imprimirlas? Los responsables de OpenGL ya pensaron en esta situación y crearon esta opción para que sólo se impriman los polígonos que están de cara a la cámara. Como nuestro juego es en 2D siempre se van a ver todos. Pero es bueno activarlo por si alguna vez metemos una rotación.

Le indicamos que el fondo de la pantalla va a ser de color negro:

glClearColor(0, 0, 0, 0);

El cuarto componente es el canal alfa (la transparencia). Luego configuramos la profundidad del Z-Buffer:

glClearDepth(1);

El Z-Buffer en este caso es muy importante. Determina el orden de superposición de polígonos. Si no lo activamos lo mismo aparece un enemigo delante del personaje que detrás, incluso podría provocar parpadeos si se cruzan dos polígonos que están en la misma coordenada Z. Ya veremos esto detenidamente con los sprites.

Fijamos la perspectiva de la cámara:

gluPerspective(60, rRatio, 1.0, 1024.0);

Esta perspectiva determina el enfoque de la cámara respecto a la escena. Podemos acercarla o alejarla a nuestro antojo. En este caso la he ajustado exactamente a la pantalla. Ahora activamos la carga de texturas:

glEnable(GL_TEXTURE_2D);

Activamos el buffer Z:

glEnable(GL_DEPTH_TEST);
glDepthMask(TRUE);

La siguiente función le indica el sentido de los vértices de los polígonos (en el sentido de las agujas del reloj):

glFrontFace(GL_CW);

Todos los polígonos que cuyo órden de los vértices esté al contrario de las agujas del reloj no se imprimirán.

A continuación fijamos el ancho y alto de la proyección en pantalla así como el sistema de coordenadas:

glViewport(0, 0, iAncho, iAlto);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0,640,0,480,-100,100);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

La librería OpenGL es tan flexible que permite establecer que rango van a tener nuestras coordenadas. En este caso van a ser de 0 a 640 horizontalmente y de 0 a 480 verticalmente. Originalmente el eje de coordenadas está en el centro de la pantalla lo que es un coñazo a menos que vayamos a hacer juegos en 3D con coordenadas de polígonos que vengan de programas como 3D Studio, Maya, etc.

Aquí la función más importante es glOrtho que pasa el sistema de coordenadas de 3D a un sistema ortogonal 2D. Si lo hubiésemos dejado en 3D daría la sensación de que las piezas se resquebrajan en las orillas de la pantalla y se quedan bien en el centro. Por ello quitamos la perspectiva tridimensional. Con este sistema, independientemente de la coordenada Z de los polígonos siempre se verá igual ante la cámara, sin profundidad.

Esto ya lo aclararé cuando llegue a la rutina de impresión de polígonos. En el próximo artículo veremos la clase TSprite y como lo convertí todo a OpenGL.

Pruebas realizadas en Delphi 7.

18 septiembre 2009

Mi primer videojuego independiente (1)

Durante una serie de artículos os voy a explicar mi pequeña experiencia en lo que se refiere a la programación de mi primer videojuego independiente y comercial así como su venta y distribución. Sobre todo voy a orientarme en la programación en Delphi.

Lo que voy a explicar viene a ser una extensión de esta serie de artículos que escribí hace tiempo sobre programación con SDL:

Programar videojuegos con la librería SDL (1)
Programar videojuegos con la librería SDL (2)
Programar videojuegos con la librería SDL (3)
Programar videojuegos con la librería SDL (4)
Programar videojuegos con la librería SDL (5)
Programar videojuegos con la librería SDL (6)
Programar videojuegos con la librería SDL (7)
Programar videojuegos con la librería SDL (8)
Programar videojuegos con la librería SDL (9)
Programar videojuegos con la librería SDL (10)
Programar videojuegos con la librería SDL (11)

Para los que crean que con Delphi sólo se pueden crear aplicaciones de gestión y utilidades les voy a demostrar que están muy equivocados. Trabajando a tiempo parcial y durante 7 meses al final pudimos hacer entre dos personas (programador y diseñador gráfico) el videojuego Pequepon Adventures que he metido en mi página web de Divernova Games.

Y todo con Delphi 7 y programando a pelo en Win32. Ni siquiera llego a utilizar el objeto Application. Podéis descargar la demo de aquí:

http://www.divernovagames.com/pequeponadventures.html

Si hubiese trabajado 8 horas al día de Lunes a Viernes sin matarme, realmente hubiera tardado unos 3 meses en realizarlo. Lo que más que costó fue el diseño de pantallas, ya que el juego tiene 30 niveles con más o menos 10 pantallas cada uno, lo que dan un total de 300 pantallas.

Aunque pueda parecer sencillo al principio, diseñar pantallas es desesperante. Para hacer un videojuego como dios manda creo que harían falta por lo menos cuatro personas (programador, diseñador de niveles, diseñador gráfico y músico).

PARA APRENDER HAY QUE EQUIVOCARSE

Cuando uno es novato en estos temas lo primero que hace es tirarse al toro sin planificar nada y teniendo las ideas más o menos claras pero sin un objetivo concreto. Durante muchos años he intentado hacer videojuegos primero en lenguaje ensamblador programando directamente los gráficos a la SVGA mediante puertos, luego en C/C++ con las librerías Allegro y SDL para terminar programando el Delphi con SDL y OpenGL. Eso sin contar con los DIV Games Studio, Fenix, GameMaker y demás hierbas.

Pero lo que pasa siempre es que empiezas el juego con ilusión y conforme van pasando los meses y va incrementándose la dificultad al final abandonas creyendo que el lenguaje o la librería gráfica que estas utilizando no son lo suficientemente buenos y comienzas a programar en otros lenguajes más potentes.

Pero el problema no esta en los lenguajes, está en nosotros mismos. Hay que comenzar con un proyecto pequeño y abarcable a corto plazo, da igual que sea cutre y pequeño, lo importante es aprender a terminarlo en un tiempo estimado y sin errores.

Podéis comenzar con algo como un Tetris, un juego de objetos ocultos o una pequeña aventura gráfica con un plazo no superior a tres meses. Una vez terminado el primer juego tendremos la moral suficiente para comenzar el siguiente con más calidad y contenidos.

Tampoco creo lo que dicen muchos que para crear un buen proyecto hay que estar motivado e ilusionado. Con el tiempo la ilusión se acaba y terminas abandonando. Creo que hay que tener una meta clara: ganar dinero. Lo demás son tonterías. Tu modelo de negocio puede basarse en hacer algo gratuito que descargue mucha gente y vivir de la publicidad, o bien cobrar por copia (arriesgándonos siempre con el pirateo).


EL DESARROLLO DE PEQUEPON ADVENTURES

Uno no se explica como un juego tan pequeño puede dar tantos quebraderos de cabeza. Y es por una sencilla razón: la falta de experiencia. Ya puedes leerte 100 libros de programación en los mejores lenguajes o en las librerías OpenGL o DirectX, pero hasta que no te pones a programar no te puedes ni imaginar los problemas que van a salir.

Después de tirarme 6 meses programando todo el videojuego, me puse a probarlo en otros equipos y me llevé un chasco impresionante. La librería SDL en modo 2D (sin utilizar aceleración gráfica) funciona muy bien cuando las pantallas son estáticas, pero si realizamos un scroll entonces el retrazo vertical de algunas tarjetas gráficas llega a crear unos cortes tan feos y parpadeantes que pueden matar a un epiléctico.

Me ofusqué buscando por Internet como activar la sincronización vertical en este modo de vídeo pero no encontré absolutamente nada. Todo el mundo decía que sólo funciona cuando la SDL dibuja polígonos OpenGL.

Así que después de una semana maldiciendo mi suerte me propuse dedicarme un mes más a cambiar todo el motor gráfico a OpenGL con las siguientes ventajas y dificultades:

- Todas las rutinas de dibujado de sprites hay que rehacerlas de nuevo.

- Hay que dibujar con polígonos con un ancho y alto que sea potencia de 2 (32x32, 64x64, 128x256, 32x128, etc.).

- Los polígonos no pueden superar un máximo de 256x256. Si se puede pero no todas las tarjetas lo soportan, por lo que si queréis que vuestro videojuego funcione en el mayor número de ordenadores posible no hay que pasarse de ese rango. Supongo que las últimas versiones de DirectX se pasarán esto por el forro.

- Las texturas de los polígonos hay que enviarlas todas de una vez a la memoria de la tarjeta de vídeo antes de comenzar a dibujar. No podemos subir o eliminar texturas en tiempo real porque el desastre puede ser impresionante.

- Por fin podemos activar el retrazo vertical.

- No es necesario crear pantallas temporales ni doble o triple buffer. OpenGL se encarga de todo.

- Aunque OpenGL puede dibujar polígonos de todo tipo yo prefiero partirlo todo en triángulos para que la tarjeta gráfica no tenga que complicarse la vida, aunque las últimas GPU ya ha hacen de todo. Por tanto, cada sprite tiene dos polígonos (triángulos).

- Al dibujar sprites por hardware el consumo de recursos es mínimo. Si ejecutaís Pequepon Adventures en modo ventana (ver Opciones) y abrís el Administrador de tareas de Windows veréis que a veces el juego llega a consumir entre un 0 y un 5% de procesador y todo a 40 frames por segundo. Esto es importantísimo en portátiles para que dure más la batería.

El resultado fue menos traumático de lo que me creía ya que aproveché el código fuente que tenía en C++ de hace años cuando intenté hacer videojuegos 3D en OpenGL con mi propio motor 3D tipo Quake, pero nunca lo terminé por lo que he hablado: el mucho abarca poco aprieta.

Así que en estos artículos pondré fragmentos de código referentes al este nuevo motor 2D pero con aceleración gráfica OpenGL. Como comprenderéis no voy a dar todo el código fuente del juego por dos razones: es un juego comercial y que son 9.700 líneas de código. Pero sí hablaré de las partes que he considerado más difíciles.


EL EDITOR DE PANTALLAS

Programar un juego no sólo es hacer un motor 2D o 3D. Tenemos que crear herramientas externas que guarden la información que necesitamos. En mi caso fue el editor de pantallas. El juego esta programado a una resolución de 640 x 480. Lo que hice fue partir la pantalla en piezas (tiles) de 40 x 40 pixels, lo que sale un total de 16 piezas horizontales y 12 verticales:

Aquí cometí mi primer error. Al principio pensamos en hacer un pequeño videojuego gratuito tipo Snowy con una sola pantalla por nivel. Pero vimos que el juego se hacía muy corto. O hacíamos las piezas más pequeñas o le metíamos más pantallas por la derecha haciendo el Pequepon saltara a la siguiente pantalla cuando llegara a la parte derecha de la pantalla.

Pero entonces se me cruzaron los cables e intenté hacer un scroll. Al principio me salió lento y horrible. Pero entonces comencé a optimizar el motor y llegué a hacer un scroll suave y aceptable que se mueve de 3 en 3 pixels.

El error cometido fue el tener que dibujar entre dos pantallas. Cuando estas entre la primera y la segunda pantalla tienes que dibujar un trozo de cada, con la dificultad que conlleva. Cuando lleguemos a la parte de mi motor 2D os diré como resolví el problema.

Lo que debería haber hecho es un mundo gigante con un buen array bidimensional que abarque todas las pantallas de ese nivel. Ya he aprendido la lección para el siguiente juego. Pero volvamos al editor.

El editor es una aplicación normal de Delphi que contiene un solo formulario:

El juego se dibuja a tres capas:

1º capa: pantalla de fondo.

2º capa: las piezas con las que choca pequepon (suelos, paredes, bloques, etc.).

3º capa: los objetos que recogemos (fresas, llave, esfera, puerta, etc.).

En la parte derecha del editor tenemos una columna para las piezas y otra para los objetos. Como hay más piezas y objetos de lo que pueden caber en pantalla, lo que hice fue meter los bitmaps de las piezas y los objetos dentro de un componente TScrollBox para poder llegar a todas moviendo la barra de desplazamiento vertical.

Si selecciono una pieza o un objeto coloco una flecha verde a su lado. Luego cuando nos vamos a la pantalla si pinchamos con el botón izquierdo del ratón dibujamos piezas y si lo hacemos con el botón derecho del ratón ponemos objetos. La primera pieza y el primer objeto los he dejado transparentes.

¿Por qué el fondo de las piezas son de color rosa? Pues porque es el color que utilizo de máscara para dibujarlas. No me interesaba el negro porque tanto los personajes como los objetos tienen el borde negro.

Entonces en el evento OnMouseDown del formulario compruebo primero si estamos dentro de la pantalla y comenzamos a dibujar:

procedure TFPrincipal.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var i, j: Integer;
begin
if Button = mbLeft then
begin
bIzquierdo := True;

// ¿Está dentro de la pantalla?
if ( x > 40 ) and ( y > 40 ) and ( x <= 680 ) and ( y <= 520 ) then
begin
i := x div 40;
j := y div 40;
EX.Caption := IntToStr( i );
EY.Caption := IntToStr( j );

Piezas[i,j] := iPieza;
DibujarPantalla;
end;
end;

if Button = mbRight then
begin
bDerecho := True;

// ¿Está dentro de la pantalla?
if ( x > 40 ) and ( y > 40 ) and ( x <= 680 ) and ( y <= 520 ) then
begin
i := x div 40;
j := y div 40;
EX.Caption := IntToStr( i );
EY.Caption := IntToStr( j );

Objetos[i,j] := iObjeto;
DibujarPantalla;
end;
end;
end;

Las piezas y los objetos son dos array bidimensionales de bytes, por lo tanto solo podemos poner un máximo de 255 piezas distintas:

var
Piezas, Objetos: array[1..16,1..12] of byte;

Esto no me preocupa porque por cada mundo vuelvo a cargar piezas nuevas:

En cambio, los objetos son iguales para todos los mundos. Aún así sólo he gastado 77 objetos de los 255 que tengo disponibles. Si necesitáis más piezas pues hacéis un array de DWord. La procedimiento de DibujarPantalla es este:

procedure TFPrincipal.DibujarPantalla;
var i, j: Integer;
Origen, Destino: TRect;
begin
// Lo dibujamos todo el el buffer

with Buffer.Canvas do
begin
// Primero dibujamos el fondo
Origen.Top := 0;
Origen.Left := 0;
Origen.Right := 640;
Origen.Bottom := 480;
Destino.Top := 0;
Destino.Left := 0;
Destino.Right := 640;
Destino.Bottom := 480;
CopyMode := cmSrcCopy;
CopyRect( Destino, ImagenFondo.Canvas, Origen );

for j := 1 to 12 do
for i := 1 to 16 do
begin
if Piezas[i,j] > 0 then
begin
Origen.Top := Piezas[i,j] * 40;
Origen.Left := 0;
Origen.Right := 40;
Origen.Bottom := Piezas[i,j] * 40 + 40;
Destino.Top := (j-1)*40;
Destino.Left := (i-1)*40;
Destino.Right := (i-1)*40 + 40;
Destino.Bottom := (j-1)*40 + 40;

// ¿La pieza que va a poner ya no existe?
if Piezas[i,j] > ImagenPiezas.Height div 40 then
begin
Brush.Color := clRed;
FillRect( Destino );
end
else
if MascaraPiezas = nil then
begin
CopyMode := cmSrcCopy;
CopyRect( Destino, ImagenPiezas.Canvas, Origen );
end
else
begin
CopyMode := cmSrcAnd;
CopyRect( Destino, MascaraPiezas.Canvas, Origen );
CopyMode := cmSrcPaint;
CopyRect( Destino, MascaraPiezas2.Canvas, Origen );
end;
end;

if Objetos[i,j] > 0 then
begin
Origen.Top := Objetos[i,j] * 40;
Origen.Left := 0;
Origen.Right := 40;
Origen.Bottom := Objetos[i,j] * 40 + 40;
Destino.Top := (j-1)*40;
Destino.Left := (i-1)*40;
Destino.Right := (i-1)*40 + 40;
Destino.Bottom := (j-1)*40 + 40;

// ¿El objeto que va a poner ya no existe?
if Objetos[i,j] > ImagenObjetos.Height div 40 then
begin
Brush.Color := clYellow;
FillRect( Destino );
end
else
if MascaraObjetos = nil then
begin
CopyMode := cmSrcCopy;
CopyRect( Destino, ImagenObjetos.Canvas, Origen );
end
else
begin
CopyMode := cmSrcAnd;
CopyRect( Destino, MascaraObjetos.Canvas, Origen );
CopyMode := cmSrcPaint;
CopyRect( Destino, MascaraObjetos2.Canvas, Origen );
//CopyRect( Destino, ImagenObjetos.Canvas, Origen );
end;
end;
end;
end;

// copiamos el buffer a pantalla
with Canvas do
begin
Origen.Top := 0;
Origen.Left := 0;
Origen.Right := 640;
Origen.Bottom := 480;
Destino.Top := 40;
Destino.Left := 40;
Destino.Right := 680;
Destino.Bottom := 520;
CopyMode := cmSrcCopy;
CopyRect( Destino, Buffer.Canvas, Origen );
end;
end;

Dibujar con el canvas de Delphi es un auténtico coñazo. Tenemos primero que cargar los sprites, crear una máscara y luego dibujar la máscara y después los sprites. Para ello tuve que declarar primero todas estas imágenes:

var
MascaraPiezas, MascaraPiezas2, Buffer: TImage;
MascaraObjetos, MascaraObjetos2: TImage;

Y para crear la máscara recorro todos los pixels de la imagen y si el color es rosa entonces invierto los pixels o los elimino:

procedure TFPrincipal.CrearMascaraPiezas;
var i, j: Integer;
begin
// Creamos la máscara de color blanco con el contorno del sprite
MascaraPiezas := TImage.Create( nil );
MascaraPiezas.Width := ImagenPiezas.Width;
MascaraPiezas.Height := ImagenPiezas.Height;

MascaraPiezas2 := TImage.Create( nil );
MascaraPiezas2.Width := ImagenPiezas.Width;
MascaraPiezas2.Height := ImagenPiezas.Height;

for j := 0 to ImagenPiezas.Height - 1 do
for i := 0 to ImagenPiezas.Width - 1 do
begin
if ImagenPiezas.Canvas.Pixels[i,j] = $FF00FF then
begin
MascaraPiezas.Canvas.Pixels[i,j] := clWhite;
MascaraPiezas2.Canvas.Pixels[i,j] := clBlack;
end
else
begin
MascaraPiezas.Canvas.Pixels[i,j] := clBlack;
MascaraPiezas2.Canvas.Pixels[i,j] := ImagenPiezas.Canvas.Pixels[i,j];
end;
end;
end;

Luego hay que acordarse de destruirlo todo al cerrar el formulario:

procedure TFPrincipal.FormDestroy(Sender: TObject);
begin
Suelos.Free;
Paredes.Free;
Matan.Free;
Buffer.Free;
MascaraObjetos.Free;
MascaraObjetos2.Free;
MascaraPiezas.Free;
MascaraPiezas2.Free;
end;

Tanto para el videojuego como para este editor utilicé el experto EurekaLog como mi compañero inseparable. Cuando seleccionamos Archivo -> Abrir cargo la pantalla:

procedure TFPrincipal.AbrirPClick(Sender: TObject);
begin
Abrir.InitialDir := sRutaDat;
Abrir.Filter := 'Pantalla (*.pan)|*.pan';

if Abrir.Execute then
begin
sArchivoPantalla := Abrir.FileName;
CargarPantalla( sArchivoPantalla );
ETituloPantalla.Caption := ExtractFileName ( Abrir.FileName );
end;
end;

La rutina de cargar pantalla debe cargar el fondo que tiene esta pantalla, las piezas, los objetos, la posición del heroe, los enemigos, etc.:

procedure TFPrincipal.CargarPantalla( sArchivo: String );
var
F: File of byte;
i, j, iNumSue, iNumPar, iNumMat, iNumSueReal, iNumParReal, iNumMatReal: Integer;
b: Byte;
begin
AssignFile( F, sArchivo );
Reset( F );

// Cargamos las piezas
for j := 1 to 12 do
for i := 1 to 16 do
Read( F, Piezas[i,j] );

// Cargamos los objetos
for j := 1 to 12 do
for i := 1 to 16 do
Read( F, Objetos[i,j] );

// Leemos el nombre de el archivo de piezas
sArchivoPiezas := '';
for i := 1 to 30 do
begin
Read( F, b );
if b <> 32 then
sArchivoPiezas := sArchivoPiezas + Chr( b );
end;
CargarPiezas( sRutaGfcs + sArchivoPiezas + '.bmp' );

// Leemos el nombre de el archivo de fondo
sArchivoFondo := '';
for i := 1 to 30 do
begin
Read( F, b );
if b <> 32 then
sArchivoFondo := sArchivoFondo + Chr( b );
end;
CargarFondo( sRutaGfcs + sArchivoFondo + '.bmp' );

// Leemos el nombre de el archivo de piezas
sArchivoObjetos := '';
for i := 1 to 30 do
begin
Read( F, b );
if b <> 32 then
sArchivoObjetos := sArchivoObjetos + Chr( b );
end;
CargarObjetos( sRutaGfcs + sArchivoObjetos + '.bmp' );

Suelos.Clear;
Paredes.Clear;
Matan.Clear;

if not Eof(F) then
begin
// Leemos el número de suelos
Read( F, b );
iNumSue := b;

// Leemos el número de paredes
Read( F, b );
iNumPar := b;

// Leemos los suelos
iNumSueReal := 0;
for i := 1 to iNumSue do
begin
if not Eof(F) then
Read( F, b );

if b < ImagenPiezas.Height div 40 then
begin
Suelos.Add( IntToStr( b ) );
Inc(iNumSueReal);
end;
end;
iNumSue := iNumSueReal;

// Leemos las paredes
iNumParReal := 0;
for i := 1 to iNumPar do
begin
if not Eof(F) then
Read( F, b );

if b < ImagenPiezas.Height div 40 then
begin
Paredes.Add( IntToStr( b ) );
Inc(iNumParReal);
end;
end;
iNumPar := iNumParReal;

// Leemos el número que matan
if not Eof(F) then
begin
Read( F, b );
iNumMat := b;

// Leemos los que matan
iNumMatReal := 0;
for i := 1 to iNumMat do
begin
if not Eof(F) then
Read( F, b );

if b < ImagenPiezas.Height div 40 then
begin
Matan.Add(IntToStr(b));
Inc(iNumMatReal);
end;
end;

iNumMat := iNumMatReal;
end;
end;

CloseFile( F );
sArchivo := Abrir.FileName;
DibujarPantalla;
MostrarSuelosParedes;
end;

Los objetos también los utilizo para colocar los enemigos en pantalla y saber que recorrido van a tener. Para delimitar los movimientos de los enemigos cree un objeto especial (la X) que se ve en el editor pero no en el juego. Cuando un enemigo encuentra este objeto da la vuelta:


Los objetos de los enemigos tampoco se verán en el juego. Me valen para saber donde comienza cada enemigo y el movimiento que va a hacer. Para hacer el editor más cómodo también hice que si dejamos pulsado los botones izquierdo o derecho del ratón podemos dibujar una columna o fila de bloques sin tener que hacer clic cada vez. Esto se hace con el evento OnMouseMove:

procedure TFPrincipal.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var i, j: Integer;
begin
// ¿Está dentro de la pantalla?
if ( x > 40 ) and ( y > 40 ) and ( x <= 680 ) and ( y <= 520 ) then
begin
i := x div 40;
j := y div 40;

if ( i < 1 ) or ( i > 16 ) or ( j < 1 ) or ( j > 12 ) then
Exit;

EX.Caption := IntToStr( i );
EY.Caption := IntToStr( j );

// ¿Está pulsado el botón izquierdo del ratón?
if bIzquierdo then
begin
Piezas[i,j] := iPieza;
DibujarPantalla;
end;

// ¿Está pulsado el botón izquierdo del ratón?
if bDerecho then
begin
Objetos[i,j] := iObjeto;
DibujarPantalla;
end;
end;
end;

Pero como tenemos que saber si hemos dejado pulsado el botón izquierdo o derecho del ratón entonces creé dos variables globales:

var
bIzquierdo: Boolean; // ¿Está pulsado el botón izquierdo del ratón?
bDerecho: Boolean; // ¿Está pulsado el botón derecho del ratón?

Y en los eventos OnMouseDown que hemos visto antes y en el evento OnMouseUp controlo cuando el usuario los ha apretado o soltado:

procedure TFPrincipal.FormMouseUp( Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer );
begin
if Button = mbLeft then
bIzquierdo := False;

if Button = mbRight then
bDerecho := False;
end;

Lo que más me costó al principio fue la rutina de guardar pantalla ya que hay que hay que guardar el nombre del bitmap de fondo, las piezas, los objetos (y enemigos) así como saber con que piezas choca el personaje y los enemigos:

procedure TFPrincipal.GuardaPantalla;
var
F: file of byte;
i, j: Integer;
Espacio, b: Byte;
begin
if sArchivoPiezas = '' then
begin
Application.MessageBox( 'Debe seleccionar el archivo de las piezas.',
'Atención', MB_ICONEXCLAMATION );
Exit;
end;

if sArchivoFondo = '' then
begin
Application.MessageBox( 'Debe seleccionar la imagen de fondo.',
'Atención', MB_ICONEXCLAMATION );
Exit;
end;

if sArchivoObjetos = '' then
begin
Application.MessageBox( 'Debe seleccionar el archivo de los objetos.',
'Atención', MB_ICONEXCLAMATION );
Exit;
end;

// Guardamos las piezas y objetos de la pantalla
AssignFile( F, sArchivoPantalla );
Rewrite( F );
Espacio := 32;

// Piezas
for j := 1 to 12 do
for i := 1 to 16 do
Write( F, Piezas[i,j] );

// Objetos
for j := 1 to 12 do
for i := 1 to 16 do
Write( F, Objetos[i,j] );

// Guardamos el nombre del archivo de las piezas
for i := 1 to 30 do
if i <= Length( sArchivoPiezas ) then
Write( F, Byte( sArchivoPiezas[i] ) )
else
Write( F, Espacio ); // espacio en blanco

// Guardamos el nombre del archivo de fondo
for i := 1 to 30 do
if i <= Length( sArchivoFondo ) then
Write( F, Byte( sArchivoFondo[i] ) )
else
Write( F, Espacio ); // espacio en blanco

// Guardamos el nombre del archivo de los objetos
for i := 1 to 30 do
if i <= Length( sArchivoObjetos ) then
Write( F, Byte( sArchivoObjetos[i] ) )
else
Write( F, Espacio ); // espacio en blanco

// Guardamos el número de suelos
b := Suelos.Count;
Write(F, b);

// Guardamos el número de paredes
b := Paredes.Count;
Write(F, b);

// Guardamos los suelos
for i := 0 to Suelos.Count-1 do
begin
b := StrToInt(Suelos[i]);
Write(F, b);
end;

// Guardamos las paredes
for i := 0 to Paredes.Count-1 do
begin
b := StrToInt(Paredes[i]);
Write(F, b);
end;

// Guardamos el número que matan
b := Matan.Count;
Write(F, b);

// Guardamos los que matan
for i := 0 to Matan.Count-1 do
begin
b := StrToInt(Matan[i]);
Write(F, b);
end;

CloseFile( F );
end;

Si os fijáis en las columnas de las piezas y los objetos a la izquierda guardo si cada pieza es libre o es suelo o pared: Estoy es muy importante para controlar si nuestro personaje va a chocar sólo verticalmente al caer o choca por todos lados, por ejemplo con el bloque de piedra. Aunque mi editor lleva mucho más código creo que he comentado las partes más importantes. Crear un buen editor de niveles es fundamental para evitar programar demasiado. En el próximo artículo comenzaré a explicar el núcleo del juego y el motor 2D creado con SDL + OpenGL.
Pruebas realizadas en Delphi 7.

11 septiembre 2009

El componente mxProtector (y 3)

Siguiendo con este fantástico componente destinado a proteger nuestras aplicaciones vamos a ver como activar un programa con una contraseña en concreto para un solo cliente. Ideal para programas a medida.

ACTIVACIÓN DE PROGRAMAS POR CLAVE DE ACCESO

El método es tan simple como no poder utilizar el programa o restringir el uso del mismo hasta que el usuario no introduzca una clave de acceso inventada por el programador y con la posibilidad de instalarlo en cualquier PC.

Para este ejemplo he creado un formulario con estos componentes:

Al arrancar el programa nos pedirá directamente la clave de acceso en el caso de que no esté registrada. Si nos equivocamos al introducirla el programa entrará en modo Demo mostrando el mensaje Programa sin registrar.

Tenemos la oportunidad de pulsar el botón Activar para introducir la clave de nuevo o bien desactivarlo por si queremos restringir de nuevo el uso del mismo a otros usuarios del mismo PC.

Comencemos configurando el componente mxProtector de este modo:

1º En la propiedad Options activamos la opción poPasswordOnce:

2º Activamos la opción stPassword en la tabla en la propiedad ProtectionTypes:

3º En el campo password escribimos nuestra contraseña:

Al pulsar la tecla Intro veremos que la contraseña se encripta en otro formato:

Ya no se si esta será la clave encriptada o su hash. A nosotros nos da lo mismo mientras un hacker no pueda adivinarla por ingeniería inversa.

4º En el evento OnGetPassword debemos preguntarle al usuario por la contraseña:

procedure TFPrincipal.mxProtectorGetPassword(Sender: TObject;
var Password: string);
begin
Password := InputBox('Introduzca la clave de activación', 'Clave:', '');
end;

5º El evento OnValidPassword se ejecutará si el usuario ha acertado la clave, por lo que deshabilitamos el botón Activar y habilitamos el botón Desactivar:

procedure TFPrincipal.mxProtectorValidPassword(Sender: TObject);
begin
EMensaje.Caption := 'Programa registrado';
BActivar.Enabled := False;
BDesactivar.Enabled := True;
end;

6º Y en el caso de que la clave sea incorrecta hacemos lo contrario dentro del evento OnWrongPassword:

procedure TFPrincipal.mxProtectorWrongPassword(Sender: TObject;
WrongPassword: string);
begin
EMensaje.Caption := 'Programa sin registrar';
BActivar.Enabled := True;
BDesactivar.Enabled := False;
Application.MessageBox('Clave de activación incorrecta',
'Consulte con su proveedor', MB_ICONSTOP);
end;

7º Para finalizar tenemos que pedir la clave al pulsar el botón Activar:

procedure TFPrincipal.BActivarClick(Sender: TObject);
begin
mxProtector.CheckPassword;
end;

8º Y eliminarla al pulsar el botón Desactivar:

procedure TFPrincipal.BDesactivarClick(Sender: TObject);
begin
mxProtector.Reset;
Application.MessageBox('Deberá introducir la clave de activación de nuevo',
'Programa desactivado', MB_ICONINFORMATION);
BActivar.Enabled := True;
BDesactivar.Enabled := False;
end;

Vamos a probarlo. Al ejecutarlo, como no está registrado lo primero que hará es pedirnos la clave:

Si nos equivocamos mostrará el mensaje de error:

Y el programa aparece en modo demo:

Pulsamos de nuevo el botón Activar, introducimos la clave correcta:

Y el programa quedará registrado:

Y aunque cerremos el programa y volvamos a abrirlo, mxProtector comprobará de nuevo si la contraseña se ha activado, por lo que no tenemos que implementar ningún código al inicio de la aplicación.

Luego podemos pulsar el botón Desactivar para eliminar la licencia en ese equipo:

Este sería uno de los métodos más básicos de venta de software a medida. Cuando te paguen le mandas la clave y activas todas las funcionalidades del programa demo.

Si nos fijamos en los proyectos de demostración que lleva este componente en su directorio demo veremos que se pueden hacer muchas más combinaciones entre proteger con contraseña, limitar el programa por número de ejecuciones, limitar por número de días o por número de serie incluyendo una clave única de hardware.

Con esto finalizo la serie de artículos dedicada al componente mxProtector. Mi propósito era contemplar por encima todos los tipos de protección que abarca así su uso a nivel de principiante sin complicaciones.

Por los ejemplos que he visto, se pueden activar varias propiedades a la vez para que el programa sea a la vez una versión demo por tiempo y una vez que el usuario pague entonces se activa según un número de serie y su contraseña, como los programas Shareware profesionales que hay en el mercado.

Lástima que los creadores de este componente vayan a cerrar la página en Diciembre de este año. Espero que dejen por ahí en algún repositorio el código fuente de este maravilloso componente por si alguien se anima a seguirlo.

Pruebas realizadas en RAD Studio 2007.

02 septiembre 2009

El componente mxProtector (2)

Vamos a continuar viendo otros modos de protección de este componente como pueden ser la limitación por número de días y por medio de licencias mediante un generador de claves.

PROTECCIÓN POR NÚMERO DE DÍAS TRANSCURRIDOS

Debemos configurar el componente mxProtector de este modo:

1º Dentro de su propiedad Options activamos las opciones poAutoInit, poCheckSystemTime y poPasswordOnce:

2º En su propiedad Protection Types debemos activar stDayTrial:

3º También debemos poner en MaxDayNumber el número de días que damos de plazo, por ejemplo 30.

3º En su evento OnDayTrial podemos el código que controla el paso de cada día:

procedure TFPrincipal.mxProtectorDayTrial(Sender: TObject;
DaysRemained: Integer);
begin
if DaysRemained = 1 then
ENumDias.Caption := 'Sólo te queda un día'
else
ENumDias.Caption := Format('Te quedan %d días.', [DaysRemained]);

BReiniciar.Enabled := False;
end;

4º En su evento OnExpiration va el código cuando ya no quedan más días:

procedure TFPrincipal.mxProtectorExpiration(Sender: TObject);
begin
ENumDias.Caption := 'Le quedan 0 días. Su licencia ha expirado';
BReiniciar.Enabled := True;
end;

5º Y en su evento OnInvalidSystemTime podemos el mensaje que muestra que el usuario ha intentado mover hacia atrás la fecha de Windows para estirar la licencia:

procedure TFPrincipal.mxProtectorInvalidSystemTime(Sender: TObject);
begin
Application.MessageBox('La hora de tu sistema no es correcta.',
'Su licencia ha expirado', MB_ICONSTOP);
end;

6º Al pulsar el botón Reiniciar volvemos a darle otros 30 días:

procedure TFPrincipal.BReiniciarClick(Sender: TObject);
begin
mxProtector.Reset;
end;

Y al igual que vimos en el ejemplo anterior de control por número de ejecuciones, podemos implementar los métodos OnGetString, OnPutString, OnGetBoolean y OnPutBoolean para que guarde el estado en la clave de registro que queramos o en un archivo INI, binario, etc. Esto es válido para todos los métodos de protección que hemos visto así como los siguientes que voy a comentar.

PROTECCIÓN POR REGISTRO DE NÚMERO DE SERIE

Esta es otra de las protecciones que más se suelen utilizar para distribuir programas con licencia Shareware. Según el nombre del usuario, un ID que haga único al PC y un número de serie podemos proteger cada licencia para que sólo se ejecute en un equipo.

Lo primero que vamos a hacer es el programa que va a registrar el usuario según un número de serie que nos dará un generador de claves que vamos a crear más adelante.

El programa va a tener este formulario:

Veamos como configurar el componente mxProtector igual que hemos visto anteriormente:

1º En la propiedad Options debemos activar las opciones poAutoInit, poCheckSystemTime, poPasswordOnce y poUseHardwareKey:


2º Activar en la propiedad ProtectionTypes el valor stRegister:


3º En el evento OnGetSerialNumber le pasamos al componente el nombre del usuario y el número de serie generado:

procedure TFPrincipal.mxProtectorGetSerialNumber(Sender: TObject;
var UserName, SerialNumber: string);
begin
UserName := Usuario.Text;
SerialNumber := NumSerie.Text;
end;

4º En el evento OnInvalidSerialNumber mostramos el mensaje de error en caso de que sea incorrecto:

procedure TFPrincipal.mxProtectorInvalidSerialNumber(Sender: TObject);
begin
Application.MessageBox('Nº de serie incorrecto',
'Consulte con el proveedor', MB_ICONSTOP);
end;

5º En el evento OnUnknowHardware debemos mostrar un mensaje en caso de que no podamos obtener la ID del PC:

procedure TFPrincipal.mxProtectorUnknownHardware(Sender: TObject);
begin
Application.MessageBox('El hardware de este equipo es incompatible con este software.',
'Consulte con el proveedor', MB_ICONSTOP);
end;

6º Al pulsar el botón Registrar activamos el producto:

procedure TFPrincipal.BRegistrarClick(Sender: TObject);
begin
mxProtector.Registration;
ComprobarRegistro;

if mxProtector.IsRegistered Then
begin
Application.MessageBox('Gracias por comprar el producto',
'Registro realizado', MB_ICONINFORMATION);
end;
end;

El procedimiento de comprobar el registro es el siguiente:

procedure TFPrincipal.ComprobarRegistro;
begin
if mxProtector.IsRegistered then
begin
Caption := 'Programa registrado';
BRegistrar.Enabled := False;
BDesinstalar.Enabled := True;
end
else
begin
Caption := 'Programa no registrado';
BRegistrar.Enabled := True;
BDesinstalar.Enabled := False;
end;
end;

Activamos o desactivamos los botones de registrar o desinstalar así como el título del formulario según este registrado el programa o no.

7º A pulsar el botón Desinstalar desinstalamos la clave de registro:

procedure TFPrincipal.BDesinstalarClick(Sender: TObject);
begin
mxProtector.Reset;
Application.MessageBox('Ya puede desinstalar del producto',
'Registro cancelado', MB_ICONINFORMATION);
ComprobarRegistro;
end;

8º Por último, tenemos que comprobar en el evento OnCreate del formulario si hemos registrado el programa y obtener ID del PC:

procedure TFPrincipal.FormCreate(Sender: TObject);
begin
ID.Text := mxProtector.GetHardwareID;
ComprobarRegistro;
end;

CREANDO EL GENERADOR DE CLAVES

Ahora vamos a hacer un programa que genere números de serie según el nombre del usuario y un ID único del PC (número de serie del disco duro, de la tarjeta de Windows, etc.).

El programa va a tener este formulario:

Veamos como configurar el componente mxProtector para este programa:

1º En la propiedad Options debemos activar las opciones poAutoInit, poCheckSystemTime, poPasswordOnce y poUseHardwareKey:


2º Activar en la propiedad ProtectionTypes el valor stRegister:

3º En el evento OnGetHardwareID nos encargamos de pasarle al componente el ID del equipo:

procedure TFPrincipal.mxProtectorGetHardwareID(Sender: TObject;
var HardwareID: string);
begin
HardwareID := ID.Text;
end;

4º El botón Generar le pedimos al componente que nos genere un número de clave según el usuario y el ID del PC:

procedure TFPrincipal.BGenerarClick(Sender: TObject);
begin
NumSerie.Text := mxProtector.GenerateSerialNumber(Usuario.Text);
end;

Vamos a probar ambos programas. Abrimos primero la aplicación:

Copiamos la clave que nos ha dado el programa, abrimos el generador de claves y escribimos nuestro nombre completo y el ID generado por el programa. Después pulsamos el botón Generar y nos dará la clave de registro:

Ahora copiamos la clave de registro, la llevamos al programa, introducimos nuestro nombre y pulsamos el botón Registrar:

Si cerramos la aplicación y volvemos a abrirla veremos que el programa ya está registrado. Podemos quitar la licencia pulsando el botón Desinstalar que dejará el programa como al principio.

Aunque parezca algo complicado, si tuviésemos que hacer todo esto a mano habría que escribir mucho más código. En el próximo artículo seguiremos viendo otros modos de protección.

Pruebas realizadas en RAD Studio 2007.

31 julio 2009

El componente mxProtector (1)

Un asunto importante que no debemos descuidar en la distribución de nuestros programas es la protección de los mismos frente a las copias piratas. Para ello hay infinidad de herramientas que permiten proteger el software por número de ejecuciones, por fecha límite, activación por número de serie, etc.

Si no queremos complicarnos mucho la vida hay herramientas que permiten proteger un ejecutable una vez compilado como pueden ser Armadillo, ASProtect, ExeCryptor, Enigma Protector, IntelliProtector, etc. Pero casi todas ellas (sobre todo las más buenas) son comerciales y ninguna es perfecta. Un buen crackeador puede reventar cualquiera de ellas utilizando ingeniería inversa mediante desensambladores y dumpeadores de memoria avanzados.

Si os interesa investigar sobre seguridad informática hay una página web en la que también tiene código fuente de Delphi dedicada a este tema:

http://indetectables.net

Hablan sobre todo de cómo crear troyanos, virus, protectores de archivos EXE o como protegerse contra los mismos. Sobre todo hay que fijarse en el foro.

DESCARGANDO EL COMPONENTE

Lo que si podemos hacer es crear una protección a nuestro programa que sin ser muy compleja por lo menos evite que cualquier principiante pueda copiarlo. Para ello vamos a ver el componente mxProtect que cumple este cometido a la perfección. La versión actual es la 1.32 (a la fecha de escribir este artículo).

Este componente lo podemos encontrar en esta página web:

http://www.maxcomponents.net/

Tiene algunos componentes comerciales y otros con licencia freeware. Es este caso, el componente mxProtect es freeware y lo podemos descargar seleccionando en su página web el apartado Downloads -> Freeware Components:


El archivo que nos bajamos es la instalación comprimida con zip que tiene un tamaño de 355 KB. Es la típica instalación de siempre donde nos pedirá donde instalar el componente (por defecto en Archivos de programa):

Yo tengo por costumbre instalar los componentes en una carpeta debajo de cada versión de Delphi, por ejemplo:

De este modo, si vamos a utilizar el componente en dos versiones distintas de Delphi (en mi caso Delphi 7 y Delphi 2007) lo mejor es que cada una tenga su copia del componente para que se compilen por separado, es decir, instalamos por ejemplo el componente mxProtector dentro de la carpeta:

D:\RadStudio2007\Componentes\mxProtector\

Y luego le sacamos una copia a mano a la carpeta:

D:\Delphi7\Componentes\mxProtector\

De este modo, cada versión del compilador no estropeará el componente de la otra.

INSTALANDO EL COMPONENTE

Una vez que lo tenemos instalado debemos añadirlo a Delphi 2007 abriendo el paquete mxProtector_11.dpk. En caso de que sea Delphi 2009 seria mxProtector_12.dpk. En la ventana del Project Manager pinchamos el archivo mxProtector_11.bpl con el botón derecho del ratón y seleccionamos Compile:


Después seleccionamos Install:


y nos aparecerá el mensaje de que acaba de instalar el componente:

Al abrir o crear un nuevo proyecto veremos ese componente en la paleta:


Para Delphi 7 sería prácticamente lo mismo.

PROTEGER UN PROGRAMA POR EL NÚMERO DE EJECUCIONES

Cada vez que se ejecute el programa restará una vez al contador de número de ejecuciones en ese PC. Vamos a ver un ejemplo de cómo crear un programa que permita ejecutarse 3 veces.

Antes tengo que aclarar una cosa. Este componente tiene dos modos de ejecución: o él se encarga de guardar (dios sabe donde) el número de ejecuciones o nosotros nos encargamos de decirle donde tiene que guardar estos datos (en un archivo INI, en un archivo binario, en el registro del sistema, etc.).

Yo prefiero esta segunda opción, ya que aunque es más pesada de implementar nos da más libertad a la hora de proteger nuestro software. Este será el método que yo voy a utilizar, concretamente lo voy a guardar en el registro del sistema dentro de la clave:

\HKEY_LOCAL_MACHINE\MiEmpresa\MiPrograma\

Por lo demás le dejamos al componente mxProtector que guarde en esa clave lo que tenga que guardar.

Para probar este ejemplo voy a comenzar un nuevo proyecto que tenga este sencillo formulario:

Al componente de la clase TMXProtector lo he llamado mxProtector y a la etiqueta que va a mostrar el número de ejecuciones que nos quedan la he llamado ENumEje.

Para ello vamos a realizar estas acciones:

1º Ponemos la propiedad MaxStartNumber del componente mxProtector a 3.

2º En el mismo componente activamos sólo el valor stStartTrial de su propiedad ProtectionType. Esto hará que se al arrancar nuestro programa se ejecute el evento OnStartTrial.

2º En el evento OnStartTrial podemos el código que se va a ejecutar por primera vez:

procedure TFPrincipal.mxProtectorStartTrial(Sender: TObject;
StartsRemained: Integer);
begin
ENumEje.Caption := Format('Te quedan %d ejecuciones de %d',
[StartsRemained, mxProtector.MaxStartNumber]);
end;

Este código se ejecutará automáticamente al inicio del programa, ya que está activada la opción poAutoInit dentro de la propiedad Options.

2º Al pulsar el botón Reiniciar ponemos a cero en número de veces que hemos ejecutado el programa:

mxProtector.Reset;

Podemos hacer que el componente se encargue de guardar ese número por si mismo o bien nos encargamos nosotros de todo el proceso (lo mejor). Como vamos a guardar en número de ejecuciones en el registro el sistema entonces haremos que en el evento OnReset elimine la clave del registro:

procedure TFPrincipal.mxProtectorReset(Sender: TObject; var Handled: Boolean);
var
Reg: TRegistry;
begin
Handled := True;
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.DeleteKey('\Software\MiEmpresa\MiPrograma');
Reg.Free;
end;

Si ponemos a True la variable Handled le estamos diciendo al componente mxProtector que nosotros nos encargamos de guardar el contador de ejecuciones. Si la ponemos a false se encarga él. Lo que no sé es donde la guarda. Lo he estado monitorizando con el programa RegMon en el registro del sistema y no he encontrado donde lo mete.

3º En el evento OnExpiration debemos añadir el código de lo que queramos que haga cuando se finalicen el número de ejecuciones (se acabe la versión trial):

procedure TFPrincipal.mxProtectorExpiration(Sender: TObject);
begin
ENumEje.Caption := 'Ya no te quedan más ejecuciones';
end;

Si por ejemplo estamos haciendo un programa de facturación podíamos cambiar o eliminar la contraseña de acceso al motor de bases de datos para que no pueda volver a funcionar el programa.

Este tipo de protección hace que este componente cargue y grabe una variable tipo booleana y otra de texto. Nosotros nos vamos a encargar de guardar y recoger estas variables. Así que vamos a reprogramar los eventos que necesitamos:

4º En el evento OnGetBoolean leemos la variable booleana que mxProtector nos pida:

procedure TFPrincipal.mxProtectorGetBoolean(Sender: TObject; var APath,
AKey: string; var AResult, Handled: Boolean);
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
try
if Reg.OpenKey('\Software\MiEmpresa\MiPrograma', True) then
if Reg.ValueExists(AKey) then
AResult := Reg.ReadBool(AKey);

Handled := True;
Reg.CloseKey;
finally
Reg.Free;
end;
end;

Al igual que antes, le ponemos la variable Handled a True para decirle al componente que nos encargamos del asunto.

5º Lo mismo hacemos para cargar una variable de tipo string en el evento OnGetString:

procedure TFPrincipal.mxProtectorGetString(Sender: TObject; var APath, AKey,
AResult: string; var Handled: Boolean);
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
try
if Reg.OpenKey('\Software\MiEmpresa\MiPrograma', True) then
if Reg.ValueExists(AKey) then
AResult := Reg.ReadString(AKey);

Handled := True;
Reg.CloseKey;
finally
Reg.Free;
end;
end;

6º Ahora hacemos lo mismo para guardar una variable booleana en el evento OnPutBoolean:

procedure TFPrincipal.mxProtectorPutBoolean(Sender: TObject; var APath,
AKey: string; var ASavedData, Handled: Boolean);
var
Reg: TRegistry;
begin
Handled := True;
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('\Software\MiEmpresa\MiPrograma', True) then
begin
Reg.WriteBool(AKey, ASavedData);
Reg.CloseKey;
end;
Reg.Free;
end;

7º Y los mismo para una variable string en el evento OnPutString:

procedure TFPrincipal.mxProtectorPutString(Sender: TObject; var APath, AKey,
ASavedData: string; var Handled: Boolean);
var
Reg: TRegistry;
begin
Handled := True;
Reg := TRegistry.Create;
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('\Software\MiEmpresa\MiPrograma', True) then
begin
Reg.WriteString(AKey, ASavedData);
Reg.CloseKey;
end;
Reg.Free;
end;

8º Por último sólo nos queda reprogramar los eventos OnCodeData y OnDecodeData:

procedure TFPrincipal.mxProtectorCodeData(Sender: TObject; var ACode: string);
begin
ACode := ACode;
end;

procedure TFPrincipal.mxProtectorDeCodeData(Sender: TObject; var ACode: string);
begin
ACode := ACode;
end;

Ahora mismo no tienen ningún tipo de codificación. Lo mismo que leen o cargan del registro del sistema es lo que va a parar al componente. Aquí podíamos ampliar la funcionalidad utilizando alguna función de encriptación y desencriptación como suele hacerse comúnmente con las funciones booleanas XOR, aunque esto se sale de los objetivos de este artículo.

Después de todo este rollo que os he metido, vamos a ejecutar el programa para ver que guarda en el registro:

Cuando se ejecuta el programa automáticamente nos resta el número de ejecuciones (eso lo hace sólo el componente) y nos guarda esto en el registro (hacer clic para ampliar):

Cerramos el programa y volvemos a abrirlo y nos queda una ejecución:

Y en el registro vemos que sólo cambia el valor S2:

Ejecutamos por última vez el programa y se acaban el nº de ejecuciones:

Y el estado del valor S2 vuelve a cambiar:


El contenido del registro puede cambiar dependiendo del PC, de la fecha y hora del sistema o de algún patrón interno del componente mxProtector. Nosotros no tenemos que preocuparnos por eso. Lo más que podemos hacer es modificar los eventos OnCodeDate y OnDecodeDate para despistar aun más a los crackers.

También podíamos encriptar el ejecutable con algún compresor de archivos EXE tipo UPX para evitar la ingenieria inversa. Aún así, ningún sistema de protección es perfecto, pero por lo menos da algo más de seguridad frente a los crackeadores novatos.

En el siguiente artículo seguiremos viendo los otros métodos de protección que incorpora este componente.

Pruebas realizadas en RAD Studio 2007.

Publicidad