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.

Publicidad