27 junio 2008

Programar videojuegos con la librería SDL (10)

CREANDO AL HEROE

Una vez tenemos el escenario vamos a crear el personaje del héroe que será el que maneje el usuario:


Para manejar el héroe de nuestro juego tenemos que declarar unas variables globales dentro de nuestra unidad UPlataformas.pas:

var
Fondo, Temporal, Piezas, Heroe: TSprite;
Mapa: array[1..16, 1..12] of char;
iPosicion, iEstado: Integer;
bMoviendo: Boolean; // ¿se esta moviendo el héroe?

La variable iPosicion va a a encargarse de controlar la posición de las piernas del héroe. Conforme vaya andando se irá incrementando la posición para dar el efecto de la animación.

La variable iEstado la vamos a utilizar la saber que está haciendo el personaje. Los posibles estados los vamos a definir en estas constantes al principio de la unidad:

const
// Estados del heroe
ANDANDO = 0;
ESCALERAS = 1;
CAYENDO = 2;
SALTANDO = 3;

La variable booleana bMoviendo va a ser nuestra bandera para controlar si el héroe se mueve o no. Si no se mueve tenemos que volver a ponerle las piernas en el suelo.

Lo siguiente a realizar es ampliar los métodos de cargar y destruir los sprites:

procedure CargarSprites;
begin
Fondo := TSprite.Create;
Fondo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) +
'fondo.jpg' );

Temporal := TSprite.Create;
Temporal.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) +
'fondo.jpg' );

Piezas := TSprite.Create;
Piezas.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) +
'piezas.png' );
Piezas.bSubsprites := True;
Piezas.iAnchoSub := 40;
Piezas.iAltoSub := 40;

Heroe := TSprite.Create;
Heroe.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) +
'heroe.png' );
Heroe.bSubsprites := True;
Heroe.iAnchoSub := 40;
Heroe.iAltoSub := 40;
Heroe.rx := 40;
Heroe.ry := 400;

CargarPantalla( 'pantalla1.txt' );
DibujarPiezasEnTemporal;
end;

procedure DestruirSprites;
begin
Heroe.Free;
Piezas.Free;
Temporal.Free;
Fondo.Free;
end;

En este juego vamos a controlar al héroe utilizando coordenadas en formato real en vez de con enteros. La razón es porque necesito más precisión para las rutinas de salto y caída al vacío.

Ahora vamos a reprogramar el procedimiento ControlarEventos para mover el héroe por pantalla:

procedure ControlarEventos;
begin
bMoviendo := False;
ComprobarDerecha;
ComprobarIzquierda;
ComprobarArriba;
ComprobarAbajo;

// Si no se esta moviendo lo dejamos en la posición inicial
if not bMoviendo then
Heroe.iSubX := 0;

// Transformarmos las coordenadas reales en formato entero
Heroe.x := Round( Heroe.rx );
Heroe.y := Round( Heroe.ry );
end;

Cada una de las direcciones en las que se mueve el héroe las he separado en un procedimiento para dar más claridad al código fuente:

procedure ComprobarDerecha;
begin
if ( Teclado.bDerecha or Joystick.bDerecha ) and
( Heroe.rx <> ESCALERAS ) then
begin
Heroe.iSubY := 0; // Hacemos que mire a la derecha
Heroe.rx := Heroe.rx + 2;
CambiarPosicion;
bMoviendo := True;
end;
end;

Cuando el usuario pulsa la tecla del cursor hacia la derecha tenemos que comprobar que no se salga de la pantalla y que no esté en las escaleras.

Igual lo hacemos hacia la izquierda:

procedure ComprobarIzquierda;
begin
if ( Teclado.bIzquierda or Joystick.bIzquierda ) and
( Heroe.rx > 40 ) and ( iEstado <> ESCALERAS ) then
begin
Heroe.iSubY := 1; // Hacemos que mire a la izquierda
Heroe.rx := Heroe.rx - 2;
CambiarPosicion;
bMoviendo := True;
end;
end;

Lo más difícil viene ahora, comprobar cuando pulsamos hacia arriba si estamos o no debajo de las escaleras:

procedure ComprobarArriba;
var
xMapa, yMapa: Integer;
begin
if ( Teclado.bArriba or Joystick.bArriba ) then
begin
// Pasamos de las coordenadas de pantalla a las coordenadas del mapa
xMapa := ( Round( Heroe.rx ) + 20 ) div 40 + 1;
yMapa := ( Round( Heroe.ry ) + 39 ) div 40 + 1;

if iEstado = ESCALERAS then
begin
Heroe.ry := Heroe.ry - 2;
CambiarPosicion;

// Si ha terminado de subir las escaleras lo dejamos de nuevo andando
if Mapa[xMapa,yMapa] <> '3' then
begin
Heroe.ry := ( yMapa - 1 ) * 40; // Ajustamos el heroe al suelo
Heroe.iSubY := 0;
iEstado := ANDANDO;
end;
end
else
begin
// ¿hay unas escaleras para subir?
if Mapa[xMapa,yMapa] = '3' then
begin
Heroe.rx := ( xMapa - 1 ) * 40; // centramos el heroe en las escaleras
Heroe.iSubY := 2;
iEstado := ESCALERAS;
end;
end;

bMoviendo := True;
end;
end;

Para comprobar esto he tenido que convertir las coordenadas del los pies del héroe en coordenadas de mapa, a fin de averiguar si tenemos delante las escaleras.

Lo mismo hay que hacer para bajar las escaleras:

procedure ComprobarAbajo;
var
xMapa, yMapa: Integer;
begin
if ( Teclado.bAbajo or Joystick.bAbajo ) then
begin
// Pasamos de las coordenadas de pantalla a las coordenadas del mapa
xMapa := ( Round( Heroe.rx ) + 20 ) div 40 + 1;
yMapa := ( Round( Heroe.ry ) + 41 ) div 40 + 1;

if iEstado = ESCALERAS then
begin
Inc( Heroe.y, 2 );
CambiarPosicion;

// Si ha terminado de subir las escaleras lo dejamos de nuevo andando
if Mapa[xMapa,yMapa] <> '3' then
begin
Heroe.ry := ( yMapa - 2 ) * 40; // Ajustamos el heroe al suelo
Heroe.iSubY := 0;
iEstado := ANDANDO;
end;
end
else
begin
// ¿hay unas escaleras para bajar?
if Mapa[xMapa,yMapa] = '3' then
begin
// centramos el heroe en las escaleras
Heroe.rx := ( xMapa - 1 ) * 40;
Heroe.iSubY := 2;
iEstado := ESCALERAS;
end;
end;

bMoviendo := True;
end;
end;

En estos cuatro procedimientos para comprobar las direcciones del héroe he llamado a un nuevo procedimiento que comprueba si una parte del mapa es sólido o no:

function Choca( x, y: Integer ): Boolean;
var
xMapa, yMapa: Integer;
begin
// Pasamos de las coordenadas de pantalla a las coordenadas del mapa
xMapa := x div 40 + 1;
yMapa := y div 40 + 1;
Result := ( Mapa[xMapa,yMapa] = '1' ) or ( Mapa[xMapa,yMapa] = '2' );
end;

Lo que hacemos es comprobar si chocamos con el suelo de piedra (1) o el de madera (2).

Otro procedimiento que también llamo es el siguiente:

procedure CambiarPosicion;
begin
Inc( iPosicion );

if iPosicion > 4 then
begin
if Heroe.iSubX = 0 then
Heroe.iSubX := 1
else
Heroe.iSubX := 0;

iPosicion := 0;
end;
end;

Lo que hace es cambiar la posición del héroe al andar o al subir las escaleras. Cada vez que el personaje se mueve 4 pixels volvemos a cambiar de posición.

LA LEY DE LA GRAVEDAD

Para darle más emoción al juego vamos a hacer unos agujeros en el mapa para que el héroe tenga que saltar por encima de ellos, aunque lo primero que tenemos que hacer es que se caiga por ellos.

Como vimos en el artículos anterior, el mapa se guardar en un archivos de texto llamado pantalla1.txt. A ese archivo le vamos a hacer unos agujeros en el suelo para que el héroe tenga que saltar:

1111111111111111
1000000000000001
1000400000000001
1222222022322221
1000000000300001
1000000000300041
1232202222222221
1030000000000001
1030004000000001
1222222202232221
1000000000030041
1111111111111111

El procedimiento ComprobarSiCae va a encargarse de estar continuamente vigilando si debajo del héroe hay un suelo al que agarrarse:

procedure ComprobarSiCae;
var
xMapa, yMapa: Integer;
begin
if ( iEstado = ESCALERAS ) or ( iEstado = SALTANDO ) then
Exit;

// Pasamos de las coordenadas de pantalla a las coordenadas del mapa
xMapa := ( Round( Heroe.rx ) + 20 ) div 40 + 1;
yMapa := ( Round( Heroe.ry ) + 40 ) div 40 + 1;

// ¿No estaba cayendo anteriormente?
if iEstado <> CAYENDO then
// Inicializamos la aceleración
rAceleracion := 0.1;

// ¿No ha chocado con el suelo?
if ( Mapa[xMapa,yMapa] <> '1' ) and ( Mapa[xMapa,yMapa] <> '2' ) and
( Mapa[xMapa,yMapa] <> '3' ) then
begin
iEstado := CAYENDO;
Heroe.ry := Heroe.ry + rAceleracion;

if rAceleracion <= 4 then
rAceleracion := rAceleracion + 0.5;
end
else
begin
iEstado := ANDANDO;
// Ajustamos el heroe al suelo
Heroe.ry := ( yMapa - 2 ) * 40;
end;
end;

Este procedimiento hay que llamarlo dentro de ControlarEventos:

procedure ControlarEventos;
begin
bMoviendo := False;
ComprobarDerecha;
ComprobarIzquierda;
ComprobarArriba;
ComprobarAbajo;
ComprobarSiCae;
...
end;

SALTANDO OBSTÁCULOS

El último movimiento que le vamos a hacer al héroe es el salto. Realmente sólo hay que controlar la mitad de salto, una vez termina la subida empieza a caer. El salto lo vamos a realizar si el usuario pulsa la barra de espacio:

procedure ControlarEventos;
begin
bMoviendo := False;
ComprobarDerecha;
ComprobarIzquierda;
ComprobarArriba;
ComprobarAbajo;
ComprobarSiCae;
ComprobarSiSalta;

if ( Teclado.bEspacio or Joystick.Boton[0] ) and
( iEstado = ANDANDO ) then
begin
rAceleracion := 5;
iEstado := SALTANDO;
end;

// Si no se esta moviendo lo dejamos en la posición inicial
if not bMoviendo then
Heroe.iSubX := 0;

// Transformarmos las coordenadas reales en formato entero
Heroe.x := Round( Heroe.rx );
Heroe.y := Round( Heroe.ry );
end;

Hemos añadido la llamada al procedimiento ComprobarSiSalta para comprobar en todo momento la subida y la deceleración:

procedure ComprobarSiSalta;
begin
if iEstado = SALTANDO then
if rAceleracion > 0 then
begin
Heroe.ry := Heroe.ry - rAceleracion;
rAceleracion := rAceleracion - 0.4;
end
else
iEstado := CAYENDO;
end;

Para hacer la caída lo más suave posible primero creo una aceleración pequeña y luego incremento la aceleración hasta 4 pixels por movimiento. Cuando choque con el suelo lo volvemos a dejar andando.

Conforme sube hacia arriba decrementamos la aceleración hasta llegar a cero. Luego cambiamos el estado a CAYENDO y dejamos que nuestro código haga el resto.

EL DINERO ES LO PRIMERO

El objetivo del juego va a ser recoger las joyas sin que nos pillen los enemigos. Lo que hay que hacer es un procedimiento que compruebe si donde está el héroe hay una joya. Cuando la encuentre la eliminamos del mapa y volvemos a dibujar el mapa en la pantalla Temporal antes de llevarla a la pantalla de vídeo.

Vamos por pasos. Primero hacemos el procedimiento que recoge las joyas:

procedure ComprobarTesoro;
var
xMapa, yMapa: Integer;
begin
// Pasamos de las coordenadas de pantalla a las coordenadas del mapa
xMapa := ( Round( Heroe.rx ) + 20 ) div 40 + 1;
yMapa := ( Round( Heroe.ry ) + 20 ) div 40 + 1;

// ¿Ha encontrado una joya?
if Mapa[xMapa,yMapa] = '4' then
begin
// la quitamos del mapa
Mapa[xMapa,yMapa] := '0';

// copiamos la pantalla de fondo a la pantalla temporal
Fondo.DibujarEn( Temporal.Superficie );

// Dibujamos las piezas en la pantalla temporal
DibujarPiezasEnTemporal;
end;
end;

Lo que hace es mirar en el mapa si estamos encima de una joya. Si es así entonces la elimina del mapa y refresca la pantalla.

Este procedimiento hay que llamarlo desde ControlarEventos:

procedure ControlarEventos;
begin
bMoviendo := False;
ComprobarDerecha;
ComprobarIzquierda;
ComprobarArriba;
ComprobarAbajo;
ComprobarSiCae;
ComprobarSiSalta;
ComprobarTesoro;

if ( Teclado.bEspacio or Joystick.Boton[0] ) and
( iEstado = ANDANDO ) then
begin
rAceleracion := 5;
iEstado := SALTANDO;
end;

// Si no se esta moviendo lo dejamos en la posición inicial
if not bMoviendo then
Heroe.iSubX := 0;

// Transformarmos las coordenadas reales en formato entero
Heroe.x := Round( Heroe.rx );
Heroe.y := Round( Heroe.ry );
end;


Este es el resultado final del juego:


Aunque donde mejor se aprecia es ejecutando mi proyecto. Aquí lo tenéis comprimido con ZIP en los tres servidores de siempre:

https://mega.nz/file/5FhU2SyD#ZavwnR9Amwcpub4wGaTI5zLsqEOPmUxuy201rtIUaOA

En el próximo artículo meteremos los enemigos en pantalla y terminaré esta serie de artículos dedicada a la programación en SDL.

Pruebas realizadas en Delphi 7.

6 comentarios:

Anónimo dijo...

Realmente estás dando en el clavo con esta sección. Enhorabuena, has conseguido que a mis 40 años me haya entrado de nuevo el gusanillo de programar juegos, como cuando con 16 años me peleaba con mi ZX Spectrum para conseguir hacer que un muñequito de 8x8 pixels se moviese :)

Lo que más me gusta, es el empeño que das a la hora de explicarlo todo. Repito, enhorabuena y espero el siguiente capítulo

Administrador dijo...

Me alegro de que te guste este blog Jose Luís. Yo también me crié con mi Sony MSX de 64 KB a base de cintas de cassete.

Entonces había que programar en Z80 a pelo en ensamblador y con ganchos de interrupción en basic.

Eso si que eran buenos tiempos. Como no había Internet teníamos que mardarnos las cintas por correo postal (y se quejan ahora del Emule, jejeje).

Ahora trato de recordar en Delphi los juegecillos que intentaba hacer por aquel entonces. La semana que viene terminaré con la SDL y comenzaré con Rad Studio 2007.

Quizás luego vuelva a la SDL para programación en 3D con OpenGL, lo que pasa es que tengo la librería todavía a medio. Poco a poco.

Saludos.

Cristian dijo...

Excelente blog!
da gusto leerlo.

Unknown dijo...

A mi tambien me encanta tu blog :)
Tengo conocimientos de delphi y desde hace tiempo he intentado trabajar con librerias sdl o directx SIN éxito.Muchas gracias por el pedazo de tutorial que te has currado.

Jorge Giraldo dijo...

Estimado Administrador: Estoy muy interesado en conocer los codigos completos de los juegos que desarrollaste con SDL. Desafortunadamente las páginas en que los cargaste: megaupload, rapidshare e hyperupload ya no existen. Existe otro sitio donde pueda descargarlos?

En todo caso mil gracias por tu blog, he aprendido muchísimo y cada vez que puedo consulto el material tan valioso que allí se encuentra. Sería magnifico saber si le has dado continuidad a este proyecto. Me parece maravilloso que a pesar de los años, hay todavía muchísima gente que continua consultando tu blog y programando en Delphi.

Te deseo muchos exitos en tus actividades.

Desde Colombia un gran abrazo.

Jorge Giraldo

Administrador dijo...

Hola Jorge,

Después de abandonar la página hace 10 años, he podido encontrar el último código fuente de los artículos que publiqué. Los he subido a MEGA y he dejado los enlaces que he podido en cada artículo dedicado a la biblioteca SDL. Espero que te sirvan de ayuda.

Aunque tengo poco tiempo libre, actualmente sigo publicando juegos retro en:

www.divernovagames.com

y aunque pueda parecer extraño, sigo utilizando Delphi 7 con la librería SDL 1 y OpenGL 1.1. Las mismas versiones que utilizé en estos artículos. El rendimiento sigue siendo fenomenal (desde Windows XP hasta Windows 10). Aunque para Windows 10 hay que parchear el EXE con un manifiesto XML con las últimas versiones de programa Resource Hacker, para que Windows 10 no fastidie el rescalado del juego. Por lo demás, creas la instalación con InnoSetup y sin problemas.

Hasta he probado mis juegos en Linux ejecutándolos con Wine y funcionan perfectamente, incluso emulando Linux con VirtualBox (con las additions incluidas).

También publiqué algunos juegos en Android con Java y Android Studio, pero en el mercado de los móviles hay que dejarte mucho dinero en publicidad para que te vean.

Y ahora estoy adaptando los juegos de Delphi a Javascript con Canvas para que se puedan jugar en la web.

Así que en este pequeño mundo de los juegos indie no te puedes aburrir.

Saludos y mucha suerte con tus creaciones.

Publicidad