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.

Publicidad