Temario/Los Sprites y los Personajes

De Tutorial LibSDL

[editar] Los Sprites y los Personajes

Tabla de contenidos


[editar] Introducción

A estas alturas de curso tenemos suficientes herramientas para enfrentarnos a la creación de un videojuego. Con un buen diseño y un poco de habilidad podremos crear nuestros propios movimientos, animaciones, control de colisiones...

En este y los capítulos sucesivos vamos a intentar que el esfuerzo que tengas que hacer para desarrollar tu primer videojuego no sea tan costoso. Vamos a ayudarte a crear animaciones, moverlas por la pantalla, interactuar con otros elementos del videojuego. En resumen, proporcionarte herramientas para que puedas llevar a cabo tus tareas pero teniendo presente que esto sólo es el principio.

Vamos a empezar presentando algunos conceptos y realizando nuestras primeras animaciones.


[editar] Objetivos

Los objetivos de este capítulo son:

  1. Conocer el concepto de sprite y los asociados a éste.
  2. Aprender a controlar los distintos tipos de animaciones.
  3. Obtener la capacidad de crear una galería.
  4. Ser capaces de controlar las colisiones en nuestra aplicación.


[editar] Sprites

Si es tu primera incursión en el desarrollo de videojuegos seguramente no sabrás qué es un sprite. El sprite se considera un de las unidades fundamentales en el desarrollo de videojuegos de dos dimensiones. Existen numerosas definiciones para este término. Nosotros nos vamos a conformar con tener un concepto sencillo y claro de lo que es un sprite.

Un sprite es un objeto que podemos visualizar en pantalla que tiene asociados ciertos atributos como puede ser la posicón, velocidad, estado... Por ejemplo, un sprite puede ser un personaje de un videojuego, el ítem que recoge del suelo, un decorado... en difinitiva cualquier representación gráfica que hagamos en el videojuego.

Los sprites en su origen fueron un tipo concreto de mapa de bits que se podían dibujar directamente por un hardware específico que liberaba de esta tarea a la CPU reduciendo la carga del sistema. Los sprites, como las imágenes que cargamos en SDL, son rectángulares. El uso de transparencias o colores clave nos permiten que estos sprites o imágenes tengan una forma distinta a la rectángular. Esta tarea era implementada por el hardware que hemos comentando realizando operaciones AND y OR entre la imagen original y la de volcado dejando el resultado en una posición de memoria que el hardware gráfico se encargaría de mostrar por pantalla.

Ejemplo de Sprite
Ejemplo de Sprite

Con el paso de los años el uso del término sprite se ha extendido a cualquier pequeño mapa de bits que se dibuje en pantalla sin tener en cuenta quien es el encargado de dibujarlo previamente. La creación de sprites ha evoluciando tanto como el mundo de la creación de videojuegos pero nosotros vamos a crear nuestros propios sprites a mano, punto por punto, digamos de una manera más "tradicional".

Actualmente los juegos en tres dimensiones copan el mercado. Esto provoca que el uso de sprite sea menor que antiguamente aunque sigue siendo fundamental en el desarrollo de gran número de juegos. La aparición de juegos para pequeños dispositivos y en formato flash provoca que "sprite" siga siendo un concepto vivo.

Como puedes ver el concepto de sprite es muy amplio. A todos esta información tenemos que añadir que también se le llama sprite a una animación, normalmente en GIF o PNG, que nos permita simular el movimiento de un personaje sea o no de videojuegos.

Los fanáticos de los sprites han protagonizado evolución que ha generado un arte propio, el conocido píxel Art. Existen numerosas comunidades dedicadas a este tipo de arte en sus distintas vertientes con un gran número de adeptos.

Y ahora lo que verdaderamente nos importa. En este curso cuando hagamos uso de la palabra sprite estaremos haciendo referencia las imágenes que representan a un personaje o a un ítem de nuestro videojuego.

Según su movimiento existen dos tipos de sprites:

  • Estáticos: Son aquellos que están fijos en la pantalla como puede ser el fondo del juego, un decorado, un objeto a coger o bien un item informativo.
  • Dinámicos: Son aquellos que se pueden mover por la pantalla o que estando estáticos tienen alguna animación asociada. Ejemplo de este tipo de sprites son los personajes de videojuegos, un objeto que realice un movimiento determinado o un objeto estático animado.

Los sprites, sobre todo los animados, pueden estar compuestos de una o varia imágenes. Cada una de estas imágenes es conocida como cuadros de animación. Comunmente en el mundo del desarrollo de videojuego cada una de estas imágenes es conocida como frame. Las animaciones de los personajes no son más que una secuencia de frames, que vistas una tras otra a una velocidad adecuada producen el efecto de movimiento o bien el de alguna acción particular.

El personaje principal de nuestro videojuego es un sprite de los llamados animados. Cuando se programan videojuegos mediante otras teconologías, como puede ser OpenGL para tres dimensiones, se utilizan técnicas distintas ya que para simular el movimiento la metodología es totalmente diferente. En este caso, más que una secuencia de imágenes, habría que realizar cálculos matemáticos que respondiesen a determinados eventos de los dispositivos de entrada o del mismo videojuego.

En un sprite se distinguen dos tipos de movimiento que debemos controlar. El movimiento externo o del sprite por la pantalla, y el movimiento interno o de animación del sprite. Para tomar la posición del sprite utilizamos la misma referencia que cuando trabajamos con imágenes ya que en definitiva se trata del mismo concepto. Recuerda que la referencia la referencia del juego se situa en la esquina superior izquierda de la pantalla, partiendo ahí el origen tanto del eje x como del eje y para nuestra aplicación.

El curso va introduciéndose cada vez más en el manejo del lenguaje C++. Para realizar un correcto manejo lo de los sprites es necesario introducirnos en la programación orientada a objetos ya que proporciona unas características deseables para este tipo de objetos y nos facilitará mucho la generación de aplicaciones dedicadas al videojuego. Si no lo has hecho aún es el momento de ponerte las pilas con este lenguaje para que el seguimiento del curso te sea más sencillo. De todas formas iremos explicando todas las novedades que aparezcan en el código.


[editar] Animando un Sprite

Para seguir este capítulo debes de tener muy presente todos los conceptos que estudiamos en el apartado dedicado al subsistema de video ya que es el punto de partida para poder realizar una animación. Vamos a manejar imágenes, superficies... No es un desarrollo complicado pero necesita que tengas asentadas las ideas que expusimos en aquel capítulo.


[editar] ¿Qué es una animación?

Podemos definir animación como una simulación de movimiento mediante una secuencia de imágenes. Al mostrar estas imágenes (llamadas cuadros o frames) sucesivamente en una misma posición producen una ilusión de movimiento que no existió en realidad. Entendemos también como animación cualquier moviminento de traslación que se haga sobre una superficie aunque el sprite que la realice no simule ningún tipo de acción.

En este principio se basan el cine o la televisión. Muestran imágenes una detrás de otra con pequeñas variaciones mediante las cuales (por el fenómeno phi) nuestro cerebro construye un movimiento rellenando los huecos entre una imagen y la siguiente. Junto con la persistencia de la retina son la base de la teoría de la representación de movimiento en cine, ordenador o televisión.

En definitiva, una animación es una secuencia de imágenes que son capaces de simular un movimiento. La vista humana tiene la limitación de poder percibir 24 imágenes por segundo lo que nos establece un punto de partida sobre el número de imágenes que necesitaremos para percibir un movimiento fluido.



[editar] Nuestra primera animación

Ya sabemos qué es una animación. Ahora vamos a crear nuestra primera animación. Para poder crear una animación necesitamos tantas imágenes como variaciones queramos que tenga dicha animación. Una vez tengamos estas imágenes iremos sustituyendo unas por otras en pantalla creando un efecto de movimiento. Fácil ¿no? Vamos a implementar una pequeña aplicación que nos permita ver como conseguimos un efecto de animación.

Para preparar estas imágenes hemos usado el editor The Gimp}. Este editor es, además de gratuito, libre. Es una de las aplicaciones de software libre que está sufriendo un mayor evolución en el mundo del tratamiento de imágenes y es más que suficiente para poder crear nuestros personajes.


[editar] Implementando una animación

Vamos a crear una animación a partir de 30 imágenes mostrándolas por pantalla una detrás de otra.

<cpp> // Ejemplo 1 // // Listado: main.cpp // Programa de pruebas. Animación básica // Primera versión. Poco eficiente


  1. include <iostream>
  2. include <iomanip>
  1. include <SDL/SDL.h>
  2. include <SDL/SDL_image.h>
  1. define TEMPO 80

using namespace std;

int main() {

   // Iniciamos el subsistema de video
   if(SDL_Init(SDL_INIT_VIDEO) < 0) {

cerr << "No se pudo iniciar SDL: " << SDL_GetError() << endl; exit(1);

   }


   atexit(SDL_Quit);
   // Comprobamos que sea compatible el modo de video
   
   if(SDL_VideoModeOK(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF) == 0) {
       cerr << "Modo no soportado: " << SDL_GetError() << endl;
       exit(1);
   }


   // Establecemos el modo de video
   SDL_Surface *pantalla;
   pantalla = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF);
   if(pantalla == NULL) {
       cerr << "No se pudo establecer el modo de video: "
            << SDL_GetError() << endl;
       exit(1);
   }


   // Cargamos las 30 imágenes de la animación
   // 30 accesos a disco (muy mejorable)
   SDL_Surface *imagenes[30];
   imagenes[29] = NULL;
   imagenes[0] = IMG_Load("Imagenes/1.png");
   imagenes[1] = IMG_Load("Imagenes/2.png");
   imagenes[2] = IMG_Load("Imagenes/3.png");
   imagenes[3] = IMG_Load("Imagenes/4.png");
   imagenes[4] = IMG_Load("Imagenes/5.png");
   imagenes[5] = IMG_Load("Imagenes/6.png");
   imagenes[6] = IMG_Load("Imagenes/7.png");
   imagenes[7] = IMG_Load("Imagenes/8.png");
   imagenes[8] = IMG_Load("Imagenes/9.png");
   imagenes[9] = IMG_Load("Imagenes/10.png");
   imagenes[10] = IMG_Load("Imagenes/11.png");
   imagenes[11] = IMG_Load("Imagenes/12.png");
   imagenes[12] = IMG_Load("Imagenes/13.png");
   imagenes[13] = IMG_Load("Imagenes/14.png");
   imagenes[14] = IMG_Load("Imagenes/15.png");
   imagenes[15] = IMG_Load("Imagenes/16.png");
   imagenes[16] = IMG_Load("Imagenes/17.png");
   imagenes[17] = IMG_Load("Imagenes/18.png");
   imagenes[18] = IMG_Load("Imagenes/19.png");
   imagenes[19] = IMG_Load("Imagenes/20.png");
   imagenes[20] = IMG_Load("Imagenes/21.png");
   imagenes[21] = IMG_Load("Imagenes/22.png");
   imagenes[22] = IMG_Load("Imagenes/23.png");
   imagenes[23] = IMG_Load("Imagenes/24.png");
   imagenes[24] = IMG_Load("Imagenes/25.png");
   imagenes[25] = IMG_Load("Imagenes/26.png");
   imagenes[26] = IMG_Load("Imagenes/27.png");
   imagenes[27] = IMG_Load("Imagenes/28.png");
   imagenes[28] = IMG_Load("Imagenes/29.png");
   // Variables auxiliares
   SDL_Event evento;
   int i = 0;
   Uint32 negro = SDL_MapRGB(pantalla->format, 0, 0, 0);
   Uint32 t0 = SDL_GetTicks();
   Uint32 t1;
   // Bucle "infinito"
   for( ; ; ) {

t1 = SDL_GetTicks();

// Mostramos una imagen cada medio segundo

if((t1 - t0) > TEMPO) {

if(i > 28) {

SDL_FillRect(pantalla, NULL, negro); SDL_Flip(pantalla);

i = 0;

} else {

SDL_BlitSurface(imagenes[i], NULL, pantalla, NULL);

SDL_Flip(pantalla);

t0 = SDL_GetTicks();

i++; } }

while(SDL_PollEvent(&evento)) {

if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE)

return 0;

if(evento.key.keysym.sym == SDLK_f) SDL_WM_ToggleFullScreen(pantalla);

}

if(evento.type == SDL_QUIT)

return 0;


}

   }   

} </cpp>

Vamos a lo novedoso del código. Como puedes ver hemos creado un vector de bajo nivel donde almacenamos cada uno de las imágenes que van a formar parte de nuestra animación. Cada vez que almacenamos una de estas imágenes necesitamos realizar un acceso a disco además de tener que especificar para cada uno de los recuadros el fichero dónde está almacenado dicho fichero.

Nos situamos en 30 lecturas de disco lo que es una carga de trabajo considerable para el sistema. Este aspecto lo mejoraremos en el siguiente apartado haciendo uso de las rejillas o panel de imágenes.

Otro aspecto destacable (negativamente) es que todo el control de la animación hemos tenido que hacer externamente. Es decir, no tenemos ningún tipo de estructura que indicando lo que queremos mostrar nos establezca una secuencialidad, un control del tiempo... y otros aspectos propios de una animación. Hemos hecho uso de las capacidades que provee SDL para el manejo del tiempo para controlar que la respuesta de la animación sea la misma en cualquier sistema donde la ejecutemos siempre que dicho sistema soporte la carga de esta aplicación. Como ya estudiamos es fundamental controlar la temporalidad de las animaciones que creemos con SDL.

En definitiva con este ejemplo no hemos más que emular el comportamiento de un giff animado con SDL pero podemos llegar mucho más lejos.


[editar] Animación Interna

Una animación interna está compuesta de un número determinado de fotogramas o imágenes sueltas que definen el comportamiento del personaje como una unidad. La animación interna se refiere a los efectos que se producen en la imagen del personaje cuando se le asocia una acción. Por ejemplo, si un personaje está caminando por la pantalla el hecho de trasladarse por dicha pantalla lo conocemos como animación externa mientras el proceso de mover las piernas, una detrás de otra, lo conocemos como animación interna.

Para almacenar estas imágenes utilizaremos una rejilla lo suficientemente grande. En ella tendremos almacenadas todas los frames de las acciones o estados que puede tener nuestro personaje. Cada una de estas animaciones tendrá asociada un número determinado de fotogramas dentro de esta rejilla de imágenes. Si lo consideramos oportuno (no te lo aconsejo) podemos tener en un solo fichero imágenes de más de un personaje o ítem. Tenemos que darle soporte a esta manera de trabajo.

Ejemplo de rejilla. Nuestro personaje principal Jacinto
Ejemplo de rejilla. Nuestro personaje principal Jacinto

Vamos a situarnos. Tenemos una rejilla con todas las imágenes que componen un personaje. Estas imágenes son de diferentes animaciones. Necesitamos una estructura que almacene una secuencia de cuadros para cada una de las animaciones, así como debe mostrar la imagen de la animación en un instante dado con lo que conseguiremos el efecto de animación deseado. Es decir necesitamos mostrar la secuencialidad de las imágenes para conseguir la animación.

Cada vez que mostremos una imagen por pantalla deberemos de borrar la anterior sino queremos ir dejando un rastro de imágenes antiguas por la superficie principal. Recuerda que el blit lo realizamos entre la superficie principal y una imagen actual. Estas imágenes van quedándose "grabadas" en dicha superficie mostrando un número de imágenes en pantalla que no tiene sentido ninguno.

La solución a este problema es intuitiva. Se trata de borrar el frame anterior antes de dibujar el siguiente, con lo que conseguiremos no dejar residuos en la pantalla. Depende del juego que estemos implementando tendremos a bien actualizar sólo una porción de pantalla o bien toda ella, teniendo en cuenta el trabajo adicional que supone para la máquina realizar cada una de estas acciones.

Necesitamos una estructura que nos almacene qué imágenes pertenecen a que animación y en qué secuencia deben ser mostradas, es decir, lo que se conoce como control lógico de la animación interna. Recuerda que la animación interna es la propia del personaje mientras que la externa es el movimiento por un determinado superficie o mapa.


[editar] Implementando el Control de la animación interna

Para implementar este control tenemos que hacer uso del concepto de clase. Vamos a usar parte de la potencia que nos brinda el lenguaje C++ para realizar un mejor trabajo de codificación que con el ejemplo anterior. Tenemos que crear una clase que nos permita, a partir de una rejilla, definir varios tipos de animaciones que mostrar por pantalla. Todo este proceso lo utilizaremos para nuestro proyecto final.

La siguente clase que vamos a presentar es el control lógico de la animación. Esta clase se encarga de llevar el control de qué posiciones o figuras de la rejilla pertenecen a una animación concreta. Todo los métodos que implementamos en ella están destinados a ofrecer la suficiente potencia para realizar esta tarea. Veamos el fichero de cabecera que define la clase:

<cpp> // Listado: Control_Animacion.h // // Esta clase controla la secuencia de animación de los personajes de la // aplicación

  1. ifndef _CONTROL_ANIMACION_H_
  2. define _CONTROL_ANIMACION_H_

const int MAX_NUM_CUADROS = 30;

class Control_Animacion {

public:
   
   // Constructor
   Control_Animacion(char *frames);
   // Consultoras
   int cuadro(void);
   bool es_primer_cuadro(void);
   
   // Modificadoras
   int avanzar(void);
   void reiniciar(void);
   // Destructor
   ~Control_Animacion();
   
private:
   int cuadros[MAX_NUM_CUADROS];
   int paso;

};

  1. endif

</cpp>


Vamos a estudiar la definición de esta clase. Entre los elementos públicos de la clase encontramos:

  • El constructor (Control Animacion()): Recibe una cadena separada por comas indicando las posiciones de la rejilla que pertenen a la animación que estamos creando. Un ejemplo de una cadena de este tipo es, por ejemplo, "1, 3, 5, 7". Con esta cadena definiríamos que la animación que estamos creando se corresponde con las posiciones 1, 3, 5 y 7 de la rejilla, que han de mostrarse en este orden, seguramente, en varias repeticiones. El parámetro retardo hace referencia al espaciado de tiempo que vamos a establecer entre mostrar un frame de la animación y el siguiente.
  • Funciones consultoras
    • int cuadro(void): Esta función devuelve el cuadro actual que debe ser mostrado en para seguir la secuencialidad de la animación.
    • bool es_primer_cuadro(void): Esta función nos devuelve si el cuadro a mostrar es el primero de la secuencia.
  • Funciones modificadoras
    • int avanzar(void): Avanza un cuadro de la animación.
    • void reiniciar(void): Coloca la animación en el primer cuadro.

En la parte privada de la clase nos encontramos con varios tipos de variables. La variable paso controla la situación actual de la animación, es decir, el paso en el que se encuentra. En el vector cuadros almacenamos la secuencialidad de las imágenes de la animación. Las otras dos variables se destinan a controlar el retardo entre frame y frame que en esta implementación no vamos a tomar en cuenta.

Veamos la implementación de estas funciones:

<cpp> // Listado: Control_Animacion.cpp // // Implementación de la clase Control Animacion

  1. include <iostream>
  2. include "Control_Animacion.h"


using namespace std;


Control_Animacion::Control_Animacion(char *frames) {

   int i = 0;
   char frames_tmp[1024];
   char *proximo;
   
   
   strcpy(frames_tmp, frames);
   // Trabajamos con una copia de los cuadros indicados
   for(proximo = strtok(frames_tmp, ","); proximo; i++){

// Desmembramos la cadena separada por comas

this->cuadros[i] = atoi(proximo); proximo = strtok(NULL, ",\0");

   }
   // Inicializamos las variables
   
   this->cuadros[i] = -1;
   this->paso = 0;
  1. ifdef DEBUG
   cout << "Control_Animacion::Control_Animacion()" << endl;
  1. endif

}


int Control_Animacion::cuadro(void) {

   return cuadros[paso];

}


int Control_Animacion::avanzar(void) {

   if(cuadros[++paso] == -1) {

paso = 0; return 1;

   }
   
   return 0;

}


void Control_Animacion::reiniciar(void) {

   // Volvemos al principio
   paso = 0;

}


bool Control_Animacion::es_primer_cuadro(void) {

   if(paso == 0)

return true;

   return false;

}

Control_Animacion::~Control_Animacion() {

  1. ifdef DEBUG
   cout << "Control_Animacion::~Control_Animacion()" << endl;
  1. endif

} </cpp>

La función que tiene una mayor carga de código es el constructor porque implementamos el bucle que nos permite separar la cadena pasada por el usuario en elementos unarios que almacenar en el vector. Lo demás no tiene mayor complejidad. En la última posición de cuadros almacenamos un -1 para saber cuando ha llegado el final de los elementos del vector.

Vamos a implementar un programa de ejemplo que nos permita hacer uso de esta clase y así comprobar la potencia de la misma. En el programa inicializaremos una secuencia cualquiera que mostraremos repetidamente. Aquí tienes el código de la misma:

<cpp> // Listado: main.cpp // // Programa de prueba de la clase Control Animación

  1. include <iostream>
  1. include "Control_Animacion.h"

using namespace std;

int main() {

   Control_Animacion animacion("0, 2, 4, 6, 8");
   cout << "Secuencia: " ;
   for(int i = 0; i < 100; i++) {

cout << " - " << animacion.cuadro();

animacion.avanzar();

   }
   cout << endl;
   return 0;

} </cpp>

En este programa de ejemplo hemos inicializado una variable del tipo Control_Animacion con unos valores determinados que marcan la secuencialidad de la animación. Luego hemos mostrado la secuencia que devuelve esta estructura para comprobar si es la correcta. Lo hacemos repetidamente para comprobar su correcto funcionamiento y efectivamente, por mucho que repitamos la consulta, nos devuelve la misma secuencia que poco más adelante será la que defina el orden de la animación.

[editar] Gestionando una rejilla de imágenes

Bien, ya tenemos una clase que nos permite controlar la secuencialidad de las imágenes de, por ejemplo, una rejilla de imágenes. Ahora necesitamos una clase que nos permita cargar una rejilla de imágenes y escoger una de las imágenes de dicha rejilla para ser mostrada por pantalla.

La idea es la siguiente. Vamos a cargar la rejilla entera en memoria. Esto nos ahorrá un número considerable de accesos a disco en comparación con la técnica utilizada en nuestra primera animación. La ventaja de este método es, principalmente, esa. Tendremos todas las imágenes en memoria en todo momento y podremos utilizarlas con cierta rapidez.

La principal desventaja del método es que ocupamos bastante mayor cantidad de memoria que con el método de cargar las imágenes sueltas. Como bien sabes el orden de velocidad de la memoria principal en un ordenador es bastante menor que el de la velocidad de la memoria secundaria por lo que la diferencia es considerable. Esto, unido a que la memoria principal actualmente no es artículo de lujo, nos lleva a evaluar esta como la mejor opción que se nos presenta.

El fichero de cabecera que nos permite gestionar las rejillas es el siguiente:

<cpp> // Listado: Imagen.h // // Clase para gestionar el trabajo con imágenes y rejillas

  1. ifndef _IMAGEN_H_
  2. define _IMAGEN_H_
  1. include <SDL/SDL.h>


class Imagen {

public:
   // Constructor
   Imagen(char *ruta, int filas = 1, int columnas = 1,\

Uint32 r = 0, Uint32 g = 255, Uint32 b = 0);

   void dibujar(SDL_Surface *superficie, int i, int x, int y, int flip = 1);
   // Consultoras
   int anchura(void);
   int altura(void);
   int cuadros(void);
   
   // Destructor 
   ~Imagen();
private:
   SDL_Surface *imagen;
   SDL_Surface *imagen_invertida;
   // Propiedades de la rejilla de la imagen   
   int columnas;
   int filas;
   // Ancho y alto por frame o recuerdo de la animación
   int w, h; 
   // Invierte la imagen en horizontal
   SDL_Surface * invertir_imagen(SDL_Surface *imagen);
   // Color clave
   Uint32 colorkey;

};

  1. endif

</cpp>


Vamos a estudiar la definición de la clase. En la parte pública de la clase encontramos lo siguiente:

fichero a cargar y el tercero el número de columnas de imágenes que contiene dicho fichero. Si queremos cargar una imagen simple basta con indicar que hay una fila de imágenes y una sóla columna, o como por omisión el valor de estos campos es uno, no sería necesario indicarlo.

En los campos r, g, b podemos definir que color queremos utilizar como color key para nuestras imágenes para cuando realicemos el blit sobre la superficie principal. Se puede establecer un color key por clase si lo consideramos oportuno. Por omisión será el color verde.

  • void dibujar(...): Esta función dibujar en la superficie que recibe como primer parámetro la imagen número i en la posición (x, y) de dicha superficie. El parámetro flip indica si queremos que la imagen sea la original o rotada verticalmente. Esto es especialmente útil para realizar animaciones en dos sentidos. No tenemos que crear al personaje volteado para que "ande" hacia el lado contrario de donde lo dibujamos.
  • int anchura(void): Devuelve la anchura de la imagen cargada en memoria.
  • int altura(void): Devuelve la altura de la imagen cargada en memoria.
  • int cuadros(void): Devuelve el número de cuadros o frames que posee la imagen cargada en memoria.
  • Destructor ~Imagen(): Libera la memoria de los elementos utilizados.

En la parte privada de la clase nos encontramos variables que almacenan información de esta clase como el número de filas y de columnas o el ancho y alto de la superficie que almacena la clase. También almacenamos la imagen invertida para los efectos de movimiento.

En esta parte de la clase se encuentra también la función encargada de obtener la imagen invertida ya que es una función interna que la clase no va a ofrecer como método de la misma.

Veamos la implementación de las funciones:

<cpp> // Listado: Imagen.cpp // // Implementación de la clase imagen para la // gestión de imágenes y rejillas

  1. include <iostream>
  2. include <SDL/SDL_image.h>
  1. include "Imagen.h"

using namespace std;


Imagen::Imagen(char *ruta, int filas, int columnas, \ Uint32 r, Uint32 g, Uint32 b) {

   this->filas = filas;
   this->columnas = columnas;
  1. ifdef DEBUG
   cout << "-> Cargando" <<  ruta << endl;
  1. endif
   // Cargamos la imagen
   imagen = IMG_Load(ruta);
   if(imagen == NULL) {

cerr << "Error: " << SDL_GetError() << endl;; exit(1);

   }
   // Convertimos a formato de pantalla
   SDL_Surface *tmp = imagen;
   
   imagen = SDL_DisplayFormat(tmp);
   SDL_FreeSurface(tmp);
   
   if(imagen == NULL) {

cerr << "Error: " << SDL_GetError() << endl; exit(1);

   }
   
   // Calculamos el color transparente, en nuestro caso el verde
   colorkey = SDL_MapRGB(imagen->format, r, g, b);
   
   // Lo establecemos como color transparente
   SDL_SetColorKey(imagen, SDL_SRCCOLORKEY, colorkey);
   
   
   // Hallamos la imagen invertida utilizada en el mayor de los casos
   // para las imágenes de vuelta
   imagen_invertida = invertir_imagen(imagen);
   if(imagen_invertida == NULL) {

cerr << "No se pudo invertir la imagen: " << SDL_GetError() << endl; exit(1);

   }
   
   // El ancho de una imagen es el total entre el número de columnas	
   w = imagen->w / columnas;
   // El ato de una imagen es el total entre el número de filas
   h = imagen->h / filas;

}


void Imagen::dibujar(SDL_Surface *superficie, int i, int x, int y, int flip) {

   SDL_Rect destino;
   
   destino.x = x;
   destino.y = y;
   // No se usan
   destino.h = 0;
   destino.w = 0;
   // Comprobamos que el número de imagen indicado sea el correcto
   if(i < 0 || i > (filas * columnas))	{

cerr << "Imagen::Dibujar = No existe el cuadro: " << i << endl; return;

   }
   SDL_Rect origen;
   // Separaciones de 2 píxeles dentro de las rejillas para observar
   // bien donde empieza una imagen y donde termina la otra
   origen.w = w - 2;
   origen.h = h - 2;
   // Seleccionamos cual de las imágenes es la que vamos a dibujar
   switch(flip) {
    case 1:

// Cálculo de la posición de la imagen // dentro de la rejilla

origen.x = ((i columnas) * w) + 2; origen.y = ((i / columnas) * h) + 2;

SDL_BlitSurface(imagen, &origen, superficie, &destino);

break;

    case -1:

// Cálculo de la posición de la imagen // dentro de la rejilla invertida

origen.x = ((columnas-1) - (i columnas)) * w + 1; origen.y = (i / columnas) * h + 2;

// Copiamos la imagen en la superficie

SDL_BlitSurface(imagen_invertida, &origen, superficie, &destino);

break;

    default:

cerr << "Caso no válido: Imagen invertida o no" << endl; break;

   }

}


// Devuelve la anchura de un cuadro de la rejilla

int Imagen::anchura() {

   return w;

}


// Devuelve la altura de un cuadro de la rejilla

int Imagen::altura() {

   return h;

}

// Devuelve el número de cuadros de la rejilla de la imagen

int Imagen::cuadros() {

   return columnas * filas;

}

Imagen::~Imagen() {

   SDL_FreeSurface(imagen);
   SDL_FreeSurface(imagen_invertida);
  1. ifdef DEBUG
   cout << "<- Liberando imagen" << endl;
  1. endif

}


SDL_Surface * Imagen::invertir_imagen(SDL_Surface *imagen) {

   SDL_Rect origen;
   SDL_Rect destino;


   // Origen -> ancho una línea
   // Comienzo de copia por el principio
   origen.x = 0;
   origen.y = 0;
   origen.w = 1;
   origen.h = imagen->h;
   // Destino -> ancho una lína
   // Comienzo de 'pegado' por el final
   // Para lograr la inversión
   destino.x = imagen->w;
   destino.y = 0;
   destino.w = 1;
   destino.h = imagen->h;
   SDL_Surface *invertida;
   // Pasamos imagen a formato de pantalla
   invertida = SDL_DisplayFormat(imagen);
   if(invertida == NULL) {

cerr << "No podemos convertir la imagen al formato de pantalla" << endl; return NULL;

   }
   // Preparamos el rectángulo nuevo vacío del color transparente
   SDL_FillRect(invertida, NULL, SDL_MapRGB(invertida->format, 0, 255, 0));
   // Copiamos linea vertical a linea vertical, inversamente
   for(int i = 0; i < imagen->w; i++) {

SDL_BlitSurface(imagen, &origen, invertida, &destino);

origen.x = origen.x + 1; destino.x = destino.x - 1;

   }
   return invertida;

} </cpp>

Como puedes ver todas las funciones están detalladamente comentadas. La implementación no tiene mayor complejidad.

Vamos a crear un programa de prueba que nos permita comprobar el funcionamiento de esta clase. Se trata de cargar una rejilla y mostrar diferentes imágenes de la misma en pantalla y así mostrar la potencia de la clase. Aquí tienes el código de la aplicación:

<cpp> // Listado: main.cpp // // Programa de prueba de la clase Imagen

  1. include <iostream>
  2. include <SDL/SDL.h>
  1. include "Imagen.h"

using namespace std;

int main() {


   // Iniciamos el subsistema de video
   if(SDL_Init(SDL_INIT_VIDEO) < 0) {

cerr << "No se pudo iniciar SDL: " << SDL_GetError() << endl; exit(1);

   }


   atexit(SDL_Quit);
   // Comprobamos que sea compatible el modo de video
   
   if(SDL_VideoModeOK(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF) == 0) {
       cerr << "Modo no soportado: " << SDL_GetError() << endl;
       exit(1);
   }


     // Establecemos el modo de video
   SDL_Surface *pantalla;
   pantalla = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF);
   if(pantalla == NULL) {
       cerr << "No se pudo establecer el modo de video: "
            << SDL_GetError() << endl;
       exit(1);
   }


   // Creamos un elemento de la clase Imagen
   Imagen personaje("./Imagenes/jacinto.bmp", 4, 7);


   // Mostramos por consola información de la imagen
   
   cout << "La imagen tiene: " << endl;
   cout << "- anchura: " << personaje.anchura() << " px"<< endl;
   cout << "- altura : " << personaje.altura() << " px" << endl;
   cout << "- cuadros: " << personaje.cuadros() << endl;
   // Mostramos varios "frames"
   // El primero normal y rotado
   personaje.dibujar(pantalla, 0, 0, 0);
   personaje.dibujar(pantalla, 0, 100, 0, -1);
   // El 10 normal y rotado
   personaje.dibujar(pantalla, 10, 100, 100);
   personaje.dibujar(pantalla, 10, 200, 100, -1); 
   // El 20 normal y rotado
   personaje.dibujar(pantalla, 20, 200, 200);
   personaje.dibujar(pantalla, 20, 300, 200, -1); 
   // El 12 normal y rotado
   personaje.dibujar(pantalla, 12, 300, 300);
   personaje.dibujar(pantalla, 12, 400, 300, -1); 
   // Actualizamos la superficie principal
   SDL_Flip(pantalla);


   // Variables auxiliares
   SDL_Event evento;
   // Bucle "infinito"
   for( ; ; ) {

while(SDL_PollEvent(&evento)) {

if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE) return 0;

}

if(evento.type == SDL_QUIT) return 0;

}

   }

} </cpp>


En este programa de prueba lo primero que hacemos es inicializar la librería SDL y establecer un modo de vídeo. Seguidamente creamos una instancia de la clase Imagen personaje con los valores correspondientes.

Lo primero que hacemos con dicha clase es mostrar toda la información que tenemos de ella. A continuación, y a modo de ejemplo, mostramos por pantalla varios de los frames} que almacena la imagen que cargamos en dicha clase. Como puedes ver esta clase nos libera de varias tareas, como la de definir una variable del tipo SDL_Rect para indicar el lugar donde mostrar la imagen cargada o establecer el color key que hemos definido a verde por omisión ya que es el que vamos a utilizar en nuestras aplicaciones.

En definitiva. Esta clase nos permite trabajar tanto con imágenes individuales como con rejillas de imágenes. Tiene la suficiente potencia para liberárnos de muchas tareas tediosas y repetitivas. Ahora bien, haciendo uso de las dos últimas clases, ¿serías capaz de implementar una clase que reproduciese una animación?


[editar] Clase Animación Interna

Vamos a unir las dos últimas clases que hemos implementado en una que nos permita crear una animación, ya sea de un personaje principal que vayamos a controlar, de un enemigo o una simple presentación a pantalla completa.

Dicha animación se basará en las imágenes de una rejilla que mostrará secuencialmente según sea el efecto que queramos poner de manifiesto. Deberá de tener un control sobre la velocidad de dicha animación para que no se convierta en un garabato en la pantalla.

Vamos a ver el fichero de cabecera de la clase:

<cpp> // Listado: Animacion.h // // Clase Animación

  1. ifndef _ANIMACION_H_
  2. define _ANIMACION_H_
  1. include <SDL/SDL.h>

// Declaración adelantada

class Imagen; class Control_Animacion;

// Clase

class Animacion {

public:
   // Constructor
   
   Animacion(char *ruta_imagen, int filas, int columnas,\

char *frames, int retardo);

   // Animación fija e infinita en pantalla
   void animar(SDL_Surface *pantalla, int x, int y, int flip = 1);
   // Dibuja la animación paso por paso
   void paso_a_paso(SDL_Surface *pantalla, int x, int y, int flip = 1);
   Uint32 retardo();
private:
   Imagen *imagen;
   Control_Animacion *control_animacion;
   Uint32 retardo_;

};

  1. endif

</cpp>

Vemos que la clase Animación en su parte pública tiene cuatro funciones. La primera de ella es el constructor al que le pasaremos la ruta de la imagen que contiene la rejilla, el número de filas y columnas de dicha rejilla, una cadea especificando qué imágenes de la rejilla formarán la animación y un retardo, que será el tiempo que transcurra entre fotograma y fotograma de la animación.

La siguiente función animar() reproduce una animación estática e infinita en la superficie que recibe como parámetro. Esta función dibujará la animación en la posición (x, y) de la pantalla.

La función paso_a_paso() imprime en pantalla el siguiente fotograma de la animación. Si en un momento dado hemos mostrado por pantalla el tercer fotograma de la animación, al llamar a esta función, nos mostrará el cuarto fotograma en la posición (x, y). Esta función nos va a ser muy útil a la hora de combinar la animación interna con la externa.

En la parte privada encontramos varias variables. Como no podía ser de otra forma en imagen y control_animacion ponemos de manifiesto el uso de estas clases dentro de nuestra nueva clase. La variable retardo_ almacena el retardo que el usuario ha establecido para las secuencias de la animación.

Vamos a estudiar la implementación de estas funciones. Veamos el código fuente de la clase:

<cpp> // Listado: Animacion.cpp // // Implementación de la clase animación

  1. include <iostream>
  1. include "Animacion.h"
  2. include "Imagen.h"
  3. include "Control_Animacion.h"

using namespace std;

Animacion::Animacion(char *ruta_imagen, int filas, int columnas,\ char *frames, int retardo_) {

   // Inicializamos las variables de la clase
   this->imagen = new Imagen(ruta_imagen, filas, columnas);
   this->control_animacion = new Control_Animacion(frames);
   this->retardo_ = retardo_;

}

void Animacion::animar(SDL_Surface *pantalla, int x, int y, int flip) {

   Uint32 t0;
   Uint32 t1;
   
   for( ; ; ) {

imagen->dibujar(pantalla, control_animacion->cuadro(),x, y, flip); control_animacion->avanzar(); SDL_Flip(pantalla);

SDL_FillRect(pantalla, NULL, \ SDL_MapRGB(pantalla->format, 0, 0, 0));

t0 = SDL_GetTicks(); t1 = SDL_GetTicks();

while((t1 - t0) < retardo_) {

t1 = SDL_GetTicks();

}

   }

}

// El control de la temporalidad tiene que se externo

void Animacion::paso_a_paso(SDL_Surface *pantalla, int x, int y, int flip){

   imagen->dibujar(pantalla, control_animacion->cuadro(), x, y, flip);
   control_animacion->avanzar();
   SDL_Flip(pantalla);

}

Uint32 Animacion::retardo() {

   return retardo_;

} </cpp>

Como puedes ver el constructor se limita a inicializar las variables que vamos a utilizar en el control y muestra por pantalla de la animación. La función animar() al mostrar una animación sin movimiento externo, controla su propio tiempo de retardo entre cuadro y cuadro haciendo uso de las funciones SDL que nos permiten tomar señales de tiempo. Va mostrando fotograma a fotograma en una posición fija.

La función paso_a_paso() no lleva el control del tiempo. No sería lógico que lo llevase. Esta función muestra un sólo fotograma, el siguiente al actual. Esto nos permite temporizar exteriormente la animación, lo que unido a que cada fotograma podemos colocarlo en la posición que más nos convenga, es la mejor opción para controlar la animación si pensamos en añadirle traslación sobre la superficie principal.

El control del tiempo es fundamental en la animación. No podemos ser dependientes de la máquina en la que se esté ejecutando nuestra aplicación para obtener una respuesta u otra de la misma. Debemos de realizar el pertinente control de tiempo. Desde que se difundió el uso de ordenadores personales es habitual encontrar en las aplicaciones de juego que se realice este control pero tiempo atrás, cuando existía una mayor variedad de sistemas propietarios, no era tan fundamental este control ya que todas las máquinas donde iba a ser ejecutado el videojuego tenían unas características similares lo que permitía que el comportamiento del videojuego fuera totalmente determinista y relativamente fácil de controlar.

Cuando desarrolles un videojuego con SDL será fundamental que se realice este control para que el videojuego sea jugado tal como diseñaste. En nuestro caso y gracias a la portabilidad que nos ofrece SDL a diferentes sistemas operativos este control se presenta aún más crítico que con otras herramientas uniplataforma. Podemos encontrarnos el caso de ejecutar nuestra apliación en dos computadoras exactamente igual a nivel hardware pero con diferentes sistemas operativos y no tener la misma respuesta del sistema. Siempre podremos retener una acción para que produzca dentro del interavalo de tiempo que nosotros queremos lo que no podemos mejorar con esta técnica es lo referente a la necesidad de unos recursos mayores.

La única forma que podemos solucionar el tema de falta de recursos es realizando un buen diseño e implementación de la aplicación para conseguir un videojuego lo más eficiente posible.

Para terminar esta sección hemos creado un pequeño programa de prueba para la clase Animación que nos permite mostrar diferentes animaciones en pantalla. El código es el siguiente:

<cpp> // Listado: main.cpp // // Programa de prueba de la clase Imagen

  1. include <iostream>
  2. include <SDL/SDL.h>
  1. include "Animacion.h"

using namespace std;

int main() {


   // Iniciamos el subsistema de video
   if(SDL_Init(SDL_INIT_VIDEO) < 0) {

cerr << "No se pudo iniciar SDL: " << SDL_GetError() << endl; exit(1);

   }


   atexit(SDL_Quit);
   // Comprobamos que sea compatible el modo de video
   
   if(SDL_VideoModeOK(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF) == 0) {
       cerr << "Modo no soportado: " << SDL_GetError() << endl;
       exit(1);
   }


     // Establecemos el modo de video
   SDL_Surface *pantalla;
   pantalla = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF);
   if(pantalla == NULL) {
       cerr << "No se pudo establecer el modo de video: "
            << SDL_GetError() << endl;
       exit(1);
   }
   // Creamos animaciones para llenar la pantalla
   Animacion animacion("./Imagenes/jacinto.bmp", 4, 7,\

"0,1,2,3,4,3,2,1,0", 120);

   Animacion animacion1("./Imagenes/jacinto.bmp", 4, 7,\

"0,15,15,0,0,15", 120);

   Animacion animacion2("./Imagenes/jacinto.bmp", 4, 7,\

"22,23,24,25", 120);

   // animacion.animar(pantalla, 100, 100);


   // Variables auxiliares
   SDL_Event evento;
   // Para controlar el tiempo
   Uint32 t0 = SDL_GetTicks();
   Uint32 t1;
   // Bucle "infinito"
   for(int x = 0, y = 0 ; ; ) {

// Si se sale de la pantalla volvemos a introducirlo

if(x == 640) x = 0;

// Referencia de tiempo

t1 = SDL_GetTicks();

if((t1 - t0) > animacion.retardo()) {

// Nueva referencia de tiempo

t0 = SDL_GetTicks();

// Movimiento del personaje

x += 4;

// Limpiamos la pantalla // Sería mejor limpiar el anterior

SDL_FillRect(pantalla, NULL,\ SDL_MapRGB(pantalla->format, 0, 0, 0));

// Mostramos el siguiente paso de todas las animaciones

animacion.paso_a_paso(pantalla, x, y); animacion1.paso_a_paso(pantalla, 200, 300); animacion2.paso_a_paso(pantalla, 300, 300); }


// Control de la entrada

while(SDL_PollEvent(&evento)) {

if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE) return 0;

}

if(evento.type == SDL_QUIT) return 0;

}

   }

} </cpp>

Vamos a lo novedoso. Una vez inicializada SDL y establecido el modo de video creamos tres variables animación para tres animaciones diferentes. Las inicializamos con los valores correspondientes observando en la imagen que almacena la rejilla qué figuras son necesarias paracada una de las animaciones.

Una vez definidas creamos unas variables que nos van a permitir controlar el tiempo de ejecución de la aplicación. Una vez en el bucle principal tenedremos dos variables x e y que nos van a aportar un control sobre el movimiento lineal (externo) que le vamos a dar a las animaciones para ir introduciéndonos en la siguiente sección.

Controlamos que ninguna de estas variables se salgan de la pantalla para que no perdamos de vista nuestras animaciones. Mediante t1 y t0 controlaremos el tiempo. Si el intervalo de tiempo es lo suficientemente grande dibujaremos el siguiente frame de nuestras animaciones en pantalla. Así de simple. Antes de dibujar los siguientes cuadros limpiaremos la pantalla para no ir dejando residuos en la misma. Este bucle se repetirá hasta que el usuario se haya cansado de observar las tres animaciones y decida salir de la aplicación.

Ya sabemos como animar un sprite en nuestra aplicación. Ahora bien, un personaje de un videojuego puede tener numerosos estados. Un estado define un comportamiento. Por ejemplo cuando un personaje anda tiene asociando un estado que es "andando". Este personaje puede en ese momento pararse, saltar, golpear... realizar ciertas acciones que lo lleven a un cambio de estado. Lo primero que tenemos que hacer al diseñar un personaje es saber en qué estados vamos a representar al personaje. Todo esto esta sujeto a una base teórica que trata sobre autómatas que estudiaremos una vez hallamos visto todos los tipos de animaciones.


[editar] Animación Externa. Actualización Lógica

Mucha de la bibliografía diferencia entre animación interna y externa. Nosotros vamos a respetar el término de animación externa aún no estando de acuerdo totalmente con él.

La animación externa es aquella que realiza el personaje al moverse por la pantalla o por el nivel. Mediante posiciones de coordenadas (x, y) podemos controlar la posición del personaje en todo momento. Para los enemigos podemos establecer un movimiento por la pantalla dado por funciones matemáticas dependiendo de la inteligencia que le queramos dotar. Para mover al personaje principal necesitaremos un control de un dispositivo de entrada para traducir las acciones sobre él en movimientos sobre la pantalla. Podemos decir que la animación externa es el cambio de posición del personaje u objeto dentro de la superficie principal que define la pantalla.

 Representación de la animación externa
Representación de la animación externa

Para mover un personaje del punto (x0, y0) al punto (x1, y1) deberemos de realizar un traslado continuo de un punto a otro con n pasos intermedios y a su vez deberemos de ir reproducciendo la animación interna del personaje dependiendo siempre del tipo de movimiento que estemos llevando a cabo, como puede ser andar, saltar, correr, agacharse... La combinación de ambos tipo de animación, siempre que se haga adecuadamente, produce un efecto de movimiento en la pantalla.

El control de este tipo de movimiento debe ser desarrollado en íntima colaboración con el desarrollo de niveles. Si estamos desarrollando un juego de plataformas deberemos de dotar al personaje de cierta gravedad a la hora de realizar saltos y cambios de nivel. Sin embargo si dotamos al juego de una perspectiva aérea tal vez tengamos que dotar al movimiento de otras fuerzas físicas diferentes a la anterior.

En un nivel de, por ejemplo, un juego de plataformas tendremos que definir que zonas de la pantalla serán tomadas como sólidas para que el personaje pueda saltar de una plataforma a otra o simplemente moverse entre ellas.


[editar] Implementando el control de la animación externa

La implementación de este control es muy dependiente del tipo de videojuego que vayamos a desarrollar. A la hora de utilizar un dispositivo de entrada, como puede ser un teclado o un joystick, para controlar el movimiento de un sprite en la pantalla tenemos dos alternativas en cuanto al conocer que acción quiere realizar el usuario.

La primera opción es trabajar con el susbsistema de eventos. Los eventos son producidos cada vez que pulsamos una tecla, movemos el ratón o interactuamos con el joystick. Tienes un apartado dedicado a la gestión de este tipo de procesos que puedes repasar para afrontar la tarea de manejar tu personaje. Las ventajas que ofrece este tipo de gestión de acciones es que no tenemos que estar realizando una espera activa para saber que ocurre si no funcionar una vez disparado un evento. El problema más grave para el manejo de personajes en un videojuego es el tiempo de vida del evento. Este tiempo de vida no nos permite, habitualmente, tener una reacción correcta a los eventos producidos por el usuario cuando interacciona con el dispositivo de entrada. El proceso de estos eventos hace que el uso general de esta técnica no se atractivo para controlar el personaje. Para teclas especiales tales como salir de la aplicación, volver a un menú, cambiar a pantalla completa y otras es adecuado utilizar este método ya que será una forma de dar preferencia a estas teclas frente a las de uso común de manejo del protagonista.

Para implementar la interacción del jugador con el usuario es más interesante un método que nos permita reaccionar a la acción actual del usuario independientemente de si hemos procesado toda la entrada o no. La segunda opción y la elegida es hacer un polling continuo al estado del teclado. Con esta técnica obtenemos un mapa instantáneo del estado del teclado con lo que podemos reaccionar a diferentes acciones. La clase que envuelvorá este proceso nos permite consultar si una tecla está presionada en un determinado momento, que es algo más lógico que preguntar si se está produciendo un evento en dicho instante ya que los eventos están pensados para ser una acción que produzcan una reacción a partir ellos y no un proceso continuo.

A favor de esta segunda opción está la implementación que vamos a presentar de los diagrama de estados de las acciones del personaje principal, que cambiará de un estado a otro según se cumplan ciertas condiciones en el dispositivo de entrada. Los diagramas de estados no son algo trivial. Estudiaremos en este curso los conceptos asociados a estos diagramas sin entrar en profundidad, sólo lo justamente necesario para realizar una correcta implementación. Lo haremos una vez estudiadas las animaciones para dar respuesta a los eventos de teclado que han de actuar sobre la animación interna. Este es un tema muy interesante en el que sería bueno que profundizaces para tu formación.

A diferencia de la animación interna no vamos a implementar una clase que nos controle el movimiento de cualquier personaje ya que por la naturaleza de las variables que necesitamos no se ajustan a la creación de una clase "Movimiento".

Vamos a crear una pequeña clase personaje, con los elementos básicos de posición, que unidos a una clase que nos controle la entrada de teclado nos permita mover nuestro personaje a través de la superficie principal. El tipo de movimiento del personaje lo definiremos en la clase del mismo personaje ya que será en esta donde tendremos el comportamiento que deba tener el personaje.

La primera clase que vamos a presentar es la que controla el teclado. Es una clase bastante sencilla que hemos implementado de una manera muy simple. El fichero donde definimos la clase es el siguiente:

<cpp> // Listado: Teclado.h // // Control del dispositivo de entrada


  1. ifndef _TECLADO_H_
  2. define _TECLADO_H_
  1. include <SDL/SDL.h>


const int NUM_TECLAS = 9;


class Teclado {

public:
   // Constructor
   Teclado();
   // Teclas a usar en la aplicación
   
   enum teclas_configuradas {

TECLA_SALIR, TECLA_SUBIR, TECLA_BAJAR, TECLA_ACEPTAR, TECLA_DISPARAR, TECLA_IZQUIERDA, TECLA_DERECHA, TECLA_SALTAR, TECLA_GUARDAR

   };
   // Consultoras
   void actualizar(void);
   bool pulso(enum teclas_configuradas tecla);
private:
   Uint8* teclas;
   SDLKey teclas_configuradas[NUM_TECLAS];

};

  1. endif

</cpp>

Como puedes observar definimos un enumerado que nos sirve para independizarnos de las constantes que usa SDL para utilizar el teclado. Hacen de interfaz entre nuestra clase y SDL. Los métodos que contiene son simples. El primero, actualizar(), es otra interfaz que nos libera de tener que llamar a la función que actualiza el estado del teclado.

El segundo, pulso(), comprueba si está pulsada una tecla que hemos pasado como parámetro. Si es así devuelve true y en caso contrario devuelve false. En cuanto a la parte privada de la clase tenemos dos variables. La primera, teclas, nos permite conocer el estado del teclado en un momento dado y la segunda teclas_configuradas} nos permite conocer qué teclas son las que hemos configurado para el manejo del personaje principal.

Vamos a ver la implementación de la clase:

<cpp> // Listado: Teclado.cpp // // Implementación de la clase teclado

  1. include <iostream>
  1. include "Teclado.h"

using namespace std;


Teclado::Teclado() {

  1. ifdef DEBUG
   cout << "Teclado::Teclado()" << endl;
  1. endif


   // Configuramos la teclas que usaremos en la aplicación
   teclas_configuradas[TECLA_SALIR] = SDLK_ESCAPE;
   teclas_configuradas[TECLA_SUBIR] = SDLK_UP;
   teclas_configuradas[TECLA_BAJAR] = SDLK_DOWN;
   teclas_configuradas[TECLA_ACEPTAR] = SDLK_RETURN;
   teclas_configuradas[TECLA_DISPARAR] = SDLK_SPACE;
   teclas_configuradas[TECLA_IZQUIERDA] = SDLK_LEFT;
   teclas_configuradas[TECLA_DERECHA] = SDLK_RIGHT;
   teclas_configuradas[TECLA_SALTAR] = SDLK_UP;
   teclas_configuradas[TECLA_GUARDAR] = SDLK_s;

}


void Teclado::actualizar(void) {

   // Actualizamos el estado del teclado mediante mapeo
   teclas = SDL_GetKeyState(NULL);

}


bool Teclado::pulso(enum teclas_configuradas tecla) {

   if(teclas[teclas_configuradas[tecla]])

return true;

   else

return false; } </cpp>

Como puedes ver no hay nada especial en la implementación. Cuando necesites implementar algo hazlo lo más sencillo que puedas, y una vez conseguido, simplificalo.

En cuanto a la clase Personaje hemos creado una clase base que nos permita principalmente tener una imagen asociada al personaje y poder establecer y modificar la posición de dicha imagen en la pantalla. Veamos la definición de la clase:

<cpp> // Listado: Personaje.h // // Clase Personaje

  1. ifndef _PERSONAJE_H_
  2. define _PERSONAJE_H_
  1. include <SDL/SDL.h>


class Personaje {

public:
   // Constructor
   Personaje(char *ruta, int x = 0, int y = 0);


   // Consultoras
   int pos_x(void);
   int pos_y(void);
   void dibujar(SDL_Surface *pantalla);
   // Modificadoras
   void pos_x(int x);
   void pos_y(int y);
   // Modifica la posición del personaje con respecto al eje X
   void avanzar_x(void);
   void retrasar_x(void);
   // Modifica la posición del personaje con respecto al eje Y
   void bajar_y(void);
   void subir_y(void);
  


private:
   
   // Posición
   int x, y;
   SDL_Surface *imagen;

};

  1. endif // _PERSONAJE_H_

</cpp>

En la implementación no añadimos ninguna novedad. En el constructor de la clase inicializamos las variables de la misma cargando la imagen en la variable correspondiente y asignando un color clave para que no se muestre el fondo verde de la imagen. El método dibujar() se encarga de pintar en la superficie que le pasemos como parámetro el personaje en las coordenadas donde se encuentre en ese momento. Las demás funciones modifican la posición del personaje según sea necesario. Establecemos el intervalo de movimiento del personaje de cuatro en cuatro píxeles para obtener un movimiento acorde con la naturaleza del personaje. La implementación de la clase es la siguiente:

<cpp> // Listado: Personaje.cpp // // Implementación de la clase Personaje

  1. include <iostream>
  2. include <SDL/SDL_image.h>
  1. include "Personaje.h"

using namespace std;


// Constructor

Personaje::Personaje(char *ruta, int x, int y) {

   this->x = x;
   this->y = y;
   // Cargamos la imagen
   imagen = IMG_Load(ruta);
   if(imagen == NULL) {

cerr << "Error: " << SDL_GetError() << endl;; exit(1);

   } 
   // Calculamos el color transparente, en nuestro caso el verde
   Uint32 colorkey = SDL_MapRGB(imagen->format, 0, 255, 0); 
   // Lo establecemos como color transparente
   SDL_SetColorKey(imagen, SDL_SRCCOLORKEY, colorkey);

}


// Consultoras

int Personaje::pos_x(void) {

   return x;

}

int Personaje::pos_y(void) {

   return y;

}

void Personaje::dibujar(SDL_Surface *pantalla) {

   SDL_Rect rect;
   rect.x = x;
   rect.y = y;
   SDL_BlitSurface(imagen, NULL, pantalla, &rect);
   

}


// Modificadoras

void Personaje::pos_x(int x) {

   this->x = x;

}


void Personaje::pos_y(int y) {

   this->y = y;

}

// El movimiento de la imagen se establece // de 4 en 4 píxeles

void Personaje::avanzar_x(void) {

   x += 4;

}

void Personaje::retrasar_x(void) {

   x -= 4;

}

void Personaje::bajar_y(void) {

   y += 4;

}

void Personaje::subir_y(void) {

   y -= 4;

} </cpp>


[editar] Actualización Lógica

Ahora vamos va combinar el uso de ambas clases en un programa principal que nos permita mover el personaje por la pantalla. Primero, como es habitual inicializaremos SDL y definiremos un objeto para cada una de las clases a utilizar. En el game loop del juego consultaremos el estado del teclado para los moviminetos del personaje y manejaremos los eventos referentes a las teclas especiales como la de salir del juego.

Cada vez que el jugador pulse una tecla se llevará acabo lo que se conoce como actualización lógica del juego. Esto es, actualizaremos el valor de la posición sin mostrarlo en pantalla. Podemos pulsar varias teclas y que el personaje se desplace lógicamente hacia arriba a la derecha en dos posiciones. Estos valores serán almacenados en la clase personaje y por cada vuelta del bucle se hará una actualización en pantalla de la posición de personaje.

Esto nos permite diferenciar dos tareas que, aunque estén relacionadas, son totalmente independientes. ¿Por qué hacemos esto así y no dibujamos el personaje cada vez que se mueve? La respuesta es simple. Imagínate que tenemos veinte elementos en pantalla que debemos de redibujar en pantalla cada vez que se mueven. Ahora vamos a suponer que se mueven una vez por segundo. En un sólo segundo tendríamos que realizar veinte actualizaciones de pantalla lo que supone un desperdicio de recursos cuando podemos trabajar de forma más eficiente.

La alternativa escogida es la siguiente. En cada iteración del game loop se realiza como mucho una actualización de la pantalla. Vamos actualizando la posición lógica de todos los personajes, objetos... que se han movido o que se están animando y al final del bucle mostramos todas las modificaciones de una sóla vez. Además añadimos una comprobación, y si no ha existido variaciones en los elementos que componen la escena no actualizamos la pantalla con lo que podemos ahorrarnos varios ciclos de actualización.

En el siguiente programa de ejemplo podemos ver como aplicar esta técnica:

<cpp> // Listados: main.cpp // // Programa de prueba

  1. include <iostream>
  2. include <SDL/SDL.h>
  1. include "Teclado.h"
  2. include "Personaje.h"

using namespace std;

int main() {

   // Iniciamos el subsistema de video
   if(SDL_Init(SDL_INIT_VIDEO) < 0) {
       cerr << "No se pudo iniciar SDL: " << SDL_GetError() << endl;
       exit(1);
   }


   atexit(SDL_Quit);
   // Comprobamos que sea compatible el modo de video
   if(SDL_VideoModeOK(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF) == 0) {
       cerr << "Modo no soportado: " << SDL_GetError() << endl;
       exit(1);
   }


   // Establecemos el modo de video
   SDL_Surface *pantalla;
   pantalla = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF);
   if(pantalla == NULL) {
       cerr << "No se pudo establecer el modo de video: "
            << SDL_GetError() << endl;
       exit(1);
   }


   // Teclado para controlar al personaje
   
   Teclado teclado;
   // Cargamos un personaje
   Personaje principal("./Imagenes/jacinto.bmp");


   // Lo mostramos por pantalla
   
   principal.dibujar(pantalla);
   SDL_Flip(pantalla);


   // Variables auxiliares
   SDL_Event evento;

   bool terminar = false;
   int x0, y0;
   // Game loop
   while(terminar == false) {

// Actualizamos el estado del teclado

teclado.actualizar();

// Variables de control para saber si // tenemos que refrescar la pantalla o no

x0 = principal.pos_x(); y0 = principal.pos_y();

// Actualización lógica de la posición

if(teclado.pulso(Teclado::TECLA_SUBIR)) {

principal.subir_y();

}

if(teclado.pulso(Teclado::TECLA_BAJAR)) {

principal.bajar_y();

}

if(teclado.pulso(Teclado::TECLA_IZQUIERDA)) {

principal.retrasar_x();

}

if(teclado.pulso(Teclado::TECLA_DERECHA)) {

principal.avanzar_x();

}


// Si existe modificación dibujamos

if(x0 != principal.pos_x() || y0 != principal.pos_y()) {

cout << "= Posición actual del personaje" << endl; cout << "- X: " << principal.pos_x() << endl; cout << "- Y: " << principal.pos_y() << endl;

// Dibujamos al personaje en su posición nueva

Uint32 negro = SDL_MapRGB(pantalla->format, 0, 0, 0);

SDL_FillRect(pantalla, NULL, negro);

principal.dibujar(pantalla);

SDL_Flip(pantalla); }


// Control de Eventos

while(SDL_PollEvent(&evento)) {


if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE) terminar = true;

}

if(evento.type == SDL_QUIT) terminar = true;

}

   }
   return 0;

} </cpp>

Vamos a centrarnos en el bucle que controla el videojuego. Lo primero que hacemos es comprobar si queremos terminar con la aplicación. En caso de que queramos salir tendremos un único punto de salida que nos permitirá una salida ordenada de la aplicación. Seguidamente actualizamos el estado teclado para que se reflejen en la variable que controla este aspecto las variaciones producidas. Lo siguiente que hacemos es tomar una instantánea de la posición del personaje para comprobar con posterioridad si ha variado la posición del mismo.

Ahora nos encontramos con una serie de estructuras selectivas que comprueban cada una de las teclas que tenemos configuradas para saber si existen variaciones. De ser así actualizaremos la posición del personaje mediante los métodos que se nos proporciona para ello. La última estructura selectiva que encontramos comprueba si el personaje ha cambiado de posición. De ser así muestra en consola un mensaje con la nueva posición, limpiamos de la pantalla la imagen anterior y mostramos el personaje en su nueva posición. Si no existen variaciones en la posición simplemente no se hace esta actualización.

Después de estas estructuras tenemos un bucle que realiza el polling de eventos por si se ha pedido salir de la aplicación hacerlo de manera ordenada.


[editar] Ejercicio 1

Vamos realizar un ejercicio de programación. Se trata de implementar una animación que vaya rebotando en los límites de la pantalla pero que nunca se salga de la misma. Utiliza la clase de personaje del ejemplo anterior. Aunque se llame personaje puede representar a cualquier imagen estática que tenga que desplazarse por la pantalla. ¡Todo tuyo!

Utilizando las clases del ejemplo anterior la solución es la siguiente:

<cpp> // Listados: main.cpp // // Ejercicio 1

  1. include <iostream>
  2. include <SDL/SDL.h>
  1. include "Personaje.h"

using namespace std;

int main() {

   // Iniciamos el subsistema de video
   if(SDL_Init(SDL_INIT_VIDEO) < 0) {
       cerr << "No se pudo iniciar SDL: " << SDL_GetError() << endl;
       exit(1);
   }


   atexit(SDL_Quit);
   // Comprobamos que sea compatible el modo de video
   if(SDL_VideoModeOK(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF) == 0) {
       cerr << "Modo no soportado: " << SDL_GetError() << endl;
       exit(1);
   }


   // Establecemos el modo de video
   SDL_Surface *pantalla;
   pantalla = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE|SDL_DOUBLEBUF);
   if(pantalla == NULL) {
       cerr << "No se pudo establecer el modo de video: "
            << SDL_GetError() << endl;
       exit(1);
   }


   // Cargamos la bola
   Personaje principal("./Imagenes/bola.bmp");


   // Lo mostramos por pantalla
   
   principal.dibujar(pantalla);
   SDL_Flip(pantalla);


   // Variables auxiliares
   SDL_Event evento;

   bool terminar = false;
   // Controlará la dirección con 
   // respecto al eje x o y
   
   bool direccion_x = true;
   bool direccion_y = false;
   int x0, y0;
   cout << "Pulse ESC para terminar" << endl;
   // Game loop
   while(terminar == false) {

// Variables de control para saber si // tenemos que refrescar la pantalla o no

x0 = principal.pos_x(); y0 = principal.pos_y();


if(direccion_x == true && principal.pos_x() >= 540) direccion_x = false;

if(direccion_y == true && principal.pos_y() >= 380) direccion_y= false;

if(direccion_x == false && principal.pos_x() <= 0) direccion_x = true;

if(direccion_y == false && principal.pos_y() <= 0) direccion_y = true;


if(direccion_x == true) principal.avanzar_x(); else principal.retrasar_x();


if(direccion_y == true) principal.bajar_y(); else principal.subir_y();




// Si existe modificación dibujamos

if(x0 != principal.pos_x() || y0 != principal.pos_y()) {

  1. ifdef DEBUG

cout << "= Posición actual del personaje" << endl; cout << "- X: " << principal.pos_x() << endl; cout << "- Y: " << principal.pos_y() << endl;

  1. endif

// Dibujamos al personaje en su posición nueva

Uint32 negro = SDL_MapRGB(pantalla->format, 0, 0, 0);

SDL_FillRect(pantalla, NULL, negro);

principal.dibujar(pantalla);

SDL_Flip(pantalla); }


// Control de Eventos

while(SDL_PollEvent(&evento)) {


if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE) terminar = true;

}

if(evento.type == SDL_QUIT) terminar = true;

}

   }
   return 0;

} </cpp>


Hemos dibujado un círculo que utilizaremos de "pelota". Con dicha imagen instanciamos la clase Personaje en una variable que vamos a llamar principal. Como ves es un ejercicio más de programación que de SDL. La lógica que sigue es la siguiente:

Tenemos dos variables lógicas que controlan la dirección de la pelota. Si están a true significa que el movimiento es hacia abajo y a la derecha. Tenemos unas estructuras selectivas que comprueban la posición y tipo de movimiento del elemento en un momento dado. Según sea este le cambia el valor a las variables para que se haga el movimiento contrario, es decir, rebote. Si por ejemplo la pelota tiene un movimiento hacia la derecha y resulta que ha llegado al límite de la pantalla pues se cambiará la variable que marca su movimiento al valor contrario del que tuvises para que vaya en la otra dirección. Esta comprobación se hace para los dos ejes por lo que se gestiona cualquier tipo de "choque" con los bordes de la superficie principal.


[editar] Integrando las animaciones

Ya hemos visto como podemos producir y controlar los tipos de animaciones interna y externa. Normalmente se integran estas dos técnicas con el fin de obtener unos mejores resultados consiguiendo un juego más atractivo.

Tenemos la necesidad de integrar ambos tipos de animaciones pero todavía no lo sabemos todo para acometer este trabajo. Hay cosas importantes que debemos de estudiar antes como los diagramas de estados de un personaje (autómatas), el cálculo de colisiones y debemos de profundizar en temas como la temporización para obtener un resultado cercano a lo que se puede desear de un videojuego.

Estos temas son suficientes por ellos mismos para crear un tutorial para cada un de ellos. Nostros vamos a dar una amplia introducción a cada uno de ellos que nos permita acometer nuestras aplicaciones SDL con ciertas garantías. Te recomiendo que profundices en cada una de estas temáticas ya que serán un gran complemento en tu formación y te dotarán de una base más solida para acometer tus desarrollos en el mundo de la programación de videojuegos.

De los aspectos más complejos de controlar de un videojuego es su temporización. De ella depende gran parte de la respuesta de la aplicación por lo que será fundamental que no dejes pasar detalle en en estudio de la misma una vez lleguemos al capítulo donde trataremos la creación del videojuego de ejemplo ya que en él realizaremos la integración de todo lo visto en el tutorial.


\section{Creando una galería}

[editar] Introducción

La mejor manera de controlar las distintas animaciones e imágenes que tiene un personaje es crear una galería. Esta galería formará parte de la clase personaje ya que será esta la que tenga que controlar de una u otra manera el estado del mismo.

El concepto de galería no necesita más explicación ya que su propia definición lo dice todo. Vamos a implementar una clase que almacene un conjunto de animaciones e imágenes que se asocien a un personaje para que tengas un ejemplo de como poder crear una galería para tu videojuego.

En el siguiente capítulo ampliaremos esta galería para que nos sea útil en nuestro videojuego de ejemplo.


[editar] Implementación de una galería

Como es habitual en el tutorial vamos a utilizar una implementación simple para desarrollar la galería. Vamos a usar aplicaciones de alto nivel y usar iteradores pero el objetivo es que captes la idea de la implementación independientemente de los detalles concretos del lenguaje o su nivel de abstracción.

Para codificar esta clase vamos a hacer uso de las clases que hemos creado a lo largo del capítulo para completar esta galería.

La definición de la clase es la siguiente:

<cpp> // Listado: Galeria.h

  1. ifndef _GALERIA_H_
  2. define _GALERIA_H_
  1. include <map>

// Declaración adelantada

class Imagen; class Fuente;


using namespace std;

class Galeria {

public:
   // Tipos de imágenes contenidas en la galería
   enum codigo_imagen {

//... Nos permite indexar las imágenes

   };
   // Fuentes almacenadas en la galería
   enum codigo_fuente {

//... Nos permite indexar las fuentes

   };


   // Constructor
   Galeria ();
   // Consultoras		

   Imagen *imagen(codigo_imagen cod_ima);
   Fuente *fuente(codigo_fuente indice);


   ~Galeria();
   // Conjunto de imágenes y de fuentes
   // que vamos a utilizar en la aplicación
   map<codigo_imagen, Imagen *> imagenes;
   map<codigo_fuente, Fuente *> fuentes;

};

  1. endif

</cpp>

En la definición de la galería tenemos varios enumerados que nos van a permitir indexar los elementos de la galería de una manera sencilla y cómoda. La clase ofrece, además del constructor y el destructor, elementos consultores que extraen la información de la galería.

Esta es una definición básica para la galería ya que supone que ésta sea estática con unos elementos insertados desde inicio. Es muy sencillo añadir a esta clase un par de métodos que nos permitan añadir elementos de forma dinámica gracias a los contenedores que utilizamos de la STL. Adapta esta clase como creas conveniente.

La implementación de la clase es la siguiente:

<cpp> // Listado: Galeria.cpp // Implementación de la clase galería del videojuego

  1. include <iostream>
  1. include "Galeria.h"
  2. include "Imagen.h"
  3. include "Fuente.h"

using namespace std;


Galeria::Galeria() {

   // Cargamos las rejillas en la galería para las animaciones
   // y las imágenes fijas
   imagenes[ /* código imagen */ ] = new Imagen( ... );
   
   // Cargamos las fuentes en la galería
   fuentes[ /* código fuente */ ] = new Fuente(...);

}


Imagen *Galeria::imagen(codigo_imagen cod_ima) {

   // Devolvemos la imagen solicitada
   return imagenes[cod_ima];

}


Fuente *Galeria::fuente(codigo_fuente indice) {

   // Devolvemos la fuente solicitada
   return fuentes[indice];

}


Galeria::~Galeria() {

   // Descargamos la galería
   delete imagenes[ /* código */ ];

...

   delete fuentes[ /* código */];

...

} </cpp>

Como puedes ver la implementación de la clase es muy sencilla. Se basa en trabajar con las aplicaciones que contiene la información deseada e insertar y extraer la información de ellas.


[editar] La animación interna y los autómatas

[editar] Introducción

La teoría de autómatas es una materia muy interesante a la que podríamos dedicarle años de estudio. En esta sección vamos a presentar una serie de conceptos muy básicos que nos van a permitir diseñar los estados en los que queremos que tenga nuestro personaje así como qué debe ocurrir para que este pase de un estado a otro.

Los personajes de videojuegos suelen realizar unos movimientos determinados como pueden ser andar, saltar, golpear... Estos movimientos suelen obtenerse al pulsar una determinada tecla o realizar una acción bien definida. En algunos videojuegos la cantidad de movimientos que se pueden obtener es mayor combinando varias acciones con diferentes teclas.

El proceso de desarrollo se hace más complicadado cuantas más acciones es capaz de hacer nuestro personaje. En esta sección vamos a estudiar como utilizar los autómatas finitos para representar el conjunto de movimientos que puede realizar el personaje. Este autómata nos será de ayuda en la codificación del mismo.

Vamos a comenzar con el concepto de estado:

[editar] Estados

Como hemos dicho un personaje, normalmente, puede realizar varios tipos de movimientos. Un estado representa el comportamiento de un personaje, objeto u otra entidad en un momento dado. Cada estado suele ser resultado de realizar o no una acción. Por ejemplo, si no pulsamos ninguna tecla ni ningún botón el personaje estará en estado de reposo o parado, pero será un estado. Cuando pulsamos la flecha a la derecha seguramente nuestro personaje andará hacia esa posición por lo que pasará al estado "andando.

Un estado puede tener asociada una imagen fija o una animación (de las conocidas como internas). Cuando el personaje está parado seguramente tengamos una imagen fija que lo represente o bien una animación con un gesto que nos haga entender que el personaje está en reposo. Como regla general para los estados que representan movimientos se suelen usar animaciones y para los estados de reposo imágenes fijas aunque no es una norma que haya que cumplir.

Los estados se unen y asocian formando autómatas. La representación de un estado en el diagrama de transiciones es un círculo o circunferencia.


[editar] Autómata

Un autómata finito se suele representar mediante un diagrama llamado de estados o de transiciones. Este diagrama es un modelo lógico asociado a una serie de propiedades. Estas propiedades son un conjunto de estados, un alfabeto que nos permite pasar de un estado a otro mediente las relaciones de transición o cambio. Una transición no es más que un cambio de estado producido por una acción o letra del alfabeto. Un alfabeto no es mas que un conjunto de símbolos. Este conjunto de símbolos puede estar definido sobre el abecedario, las teclas de un ordenador... cualquier simbología es válida.

Formalmente existen varios tipos de autómatas: autómata finito determinista (AFD), los finito no deterministas(AFND) y los autómatas finitos no deterministas con transiciones epsilon. No vamos a entrar en profundidad sobre el estudio de los autómatas por lo que no vamos a detallar en qué consiste cada uno de ellos. Es suficiente con que conozcamos que existen diversos tipos de autómatas y que, como referencia, vamos a utilizar autómatas finitos no deterministas con transiciones epsilon con el fin de diseñar los distintos estados en los que puede estar nuestro personaje y los cambios o transiciones entre ellos.

Podemos interpretar un autómata como una máquina que se encuentra en un estado determinado y que reacciona a diferentes acciones cambiando (transicionando) de un estado a otro. Esta máquina nos dirá si una entrada es válida o no dependiendo si el estado final en que se encuentre es un estado de los conocidos como de terminación o validación. En nuestro caso no vamos a utilizar esta máquina para que nos compruebe si una entrada es válida si no para tener un control sobre el comportamiento de un personaje o un objeto dentro del videojuego para que realice sólo aquellas acciones que le sean permitidas en un determinado momento.

Vamos a crear nuestro primer diagrama de transiciones. Nuestro personaje va a tener varios estados: parado, salto, andar, golpear y agachar. En nuestro caso vamos a diferenciar los estados de cuando nuestro personaje esté mirando hacia la derecha de cuando lo esté haciendo a la izquierda. Crear un diagrama de estados no es complicado. Lo primero que debes hacer es dibujar tantos círculos como estados quieras controlar. Lo siguiente es estudiar cada uno de esos estados. Céntrate en un estado y piensa a que estados es lógico que llegue tomando como partida dicho estado. Una vez lo tengas claro dibuja una flecha hacia ese estado y etiquétala con la acción que debe ocurrir para que el personaje cambie del estado inicial a este nuevo estado final. Este proceso lo repetiremos con cada uno de los estados que dibujamos inicialmente.

Al terminar marca con un ángulo el estado que quieras tomar como inicial y hazle otra circunferencia al estado que tomes como final. Con este proceso obtendrás un diagrama parecido al de la figura. Cuando quieras que se pueda pasar de un estado a otro sin que ocurra ninguna acción en la flecha que une ambos estados márcala con un epsilon lo que denotará una transición como la que quieres representar.

Ejemplo de autómata de nuestro personaje principal
Ejemplo de autómata de nuestro personaje principal

De la figura podemos interpretar las siguientes acciones:

  1. El estado inicial y final, marcados con un doble círculo y una semiflecha, es el de parado.
  2. Para situarnos en las acciones que se hacen mirando hacia la izquierda tenemos que pulsar la flecha de la izquierda mientras que para las acciones que se realizan mirando hacia la derecha tenemos que pulsar al menos una vez la tecla de la flecha a la derecha.
  3. Una vez en los estados parados a la izquierda o la derecha están determinadas las acciones (o teclas a pulsar) que hay que realizar para llegar a cada uno de los estados conectados con el anterior. Por ejemplo si pulsamos la tecla Z estando parado mirando hacia la derecha el personaje golpeará en ese sentido y una vez soltemos dicha tecla volverá al estado parado en esa dirección.

Un diagrama de transiciones no es complicado de interpretar ni de diseñar siempre que utilicemos una definición relajada del mismo. Vamos a realizar la implementación de dicho autómata.


[editar] Implementando un autómata

Existen varios métodos para implementar un autómata. Nosostros vamos a optar por una implementación clara y sencilla del mismo, como hemos hecho en el resto del tutorial, ya que el objetivo es centrarnos conocer técnicas que nos permitan implementar este tipo estructuras. Los algoritmos que utilicemos serán muy optimizables. Esta tarea ya es un ejercicio de programación que está en tu mano mejorar.

La idea es la siguiente. Vamos a implementar un algoritmo que, primero, se repita un número indeterminado de veces, es decir, hasta que el usuario quiera salir de la aplicación. Segundo, que en cada iteración permita conocer que acción estamos realizando, en nuestro caso, sobre el teclado para reaccionar según el diseño del autómata.

Para ofrecer una respuesta correcta tenemos que ampliar nuestra clase personaje añadiéndole las animaciones que vamos a utilizar en cada uno de los estados en los que se va a encontrar. Haremos uso para ello de las clases Animacion, Imagen y Control_Animacion presentadas en ejemplos anteriores. Como puedes ver los programas de ejemplo cada vez son más complejos pero estamos en el camino para crear un auténtico videojuego.

Una de las técnicas para implementar un autómata se conoce como el método de los cases. Este método se basa en utilizar una estructura selectiva del tipo switch case que tenga como discriminador el estado en el que nos encontremos. Si estamos en un determinado estado podremos realizar las acciones que se encuentre dentro de dicho case, como cambiar de estado u otra cosa. En nuestro caso si estamos en reposo estaremos en este caso del estado y podremos andar, agacharnos...

Vamos a ponernos manos a la obra. Vamos a estudiar la implementación del ejemplo que vamos a tratar. El primer fichero de código que vamos a estudiar es el de la propia implementación de lo que sería el autómata. Lo hemos incluido en el programa principal en la parte que diferenciamos como el bucle del juego. El código es el siguiente:

<cpp> // Listados: main.cpp // // Programa de prueba // Implementación INTUITIVA de autómata // Nos acerca a la implementación del autómata pero no es la // metodología más correcta

  1. include <iostream>
  2. include <SDL/SDL.h>
  1. include "Teclado.h"
  2. include "Personaje.h"
  3. include "Miscelanea.h"

using namespace std;

int main() {

   // Inicializamos SDL y Establecemos el modo de video
   SDL_Surface *pantalla;
   if(inicializar_SDL(&pantalla, "Automata")) {

cerr << "No se puede inicializar SDL" << SDL_GetError() << endl; exit(1);

   }


   // Teclado para controlar al personaje
   
   Teclado teclado;
   // Cargamos un personaje
   Personaje principal;


   // Variables auxiliares
   SDL_Event evento;
   SDL_Rect antiguo;

   bool terminar = false;
   int x0, y0;
   estados_personaje s0;
   Uint32 negro = SDL_MapRGB(pantalla->format, 0, 0, 0);
   // Controlo la dirección: true = derecha & false = izquierda
   bool direccion = true; 
   // Game loop
   while(terminar == false) {

// Actualizamos el estado del teclado

teclado.actualizar();

// Variables de control para saber si // tenemos que refrescar la pantalla o no

x0 = principal.pos_x(); y0 = principal.pos_y(); s0 = principal.estado_actual();

// Actualización lógica de la posición // y el estado

if(teclado.pulso(Teclado::TECLA_SUBIR)) {

if(direccion) principal.cambio_estado(SALTAR_DERECHA); else principal.cambio_estado(SALTAR_IZQUIERDA);

} else if(teclado.pulso(Teclado::TECLA_BAJAR)) {

if(direccion) principal.cambio_estado(AGACHAR_DERECHA); else principal.cambio_estado(AGACHAR_IZQUIERDA);

} else if(teclado.pulso(Teclado::TECLA_IZQUIERDA)) {

if(principal.pos_x() > -10) {

principal.retrasar_x(); principal.cambio_estado(ANDAR_IZQUIERDA);

}

direccion = false;

} else if(teclado.pulso(Teclado::TECLA_DERECHA)) {

if(principal.pos_x() < 580) {

principal.avanzar_x(); principal.cambio_estado(ANDAR_DERECHA);

}

direccion = true;

} else if(teclado.pulso(Teclado::TECLA_DISPARAR)) {

if(direccion) principal.cambio_estado(GOLPEAR_DERECHA); else principal.cambio_estado(GOLPEAR_IZQUIERDA);

} else {

if(direccion) principal.cambio_estado(PARADO_DERECHA); else principal.cambio_estado(PARADO_IZQUIERDA); }


// Si existe modificación dibujamos

if(x0 != principal.pos_x() || y0 != principal.pos_y() || s0 != principal.estado_actual() ) {

  1. ifdef DEBUG

cout << "= Posición actual del personaje" << endl; cout << "- X: " << principal.pos_x() << endl; cout << "- Y: " << principal.pos_y() << endl;

  1. endif

// Dibujamos al personaje en su posición nueva // Borramos la antigua

antiguo.x = x0; antiguo.y = y0;


SDL_FillRect(pantalla, NULL, negro);

principal.dibujar(pantalla);

SDL_Flip(pantalla); }

// Control de Eventos

while(SDL_PollEvent(&evento)) {


if(evento.type == SDL_KEYDOWN) {

if(evento.key.keysym.sym == SDLK_ESCAPE) terminar = true;

}

if(evento.type == SDL_QUIT) terminar = true;

}

   }
   return 0;

} </cpp>

Como puedes ver para inicializar SDL y establecer el modo de video hemos implementado una función que nos libera de realizar esta tarea. En este tutorial no hemos sido partidarios de utilizar esta función para todo ya que el objetivo de este tutorial es manejar con cierta soltura SDL y si utilizamos una función que nos libera de realizar tareas fundamentales no practicamos con dichas funciones básicas. En este caso, como la carga de código es bastante mayor, hemos decidido hacer uso de ella. Como en todos los ejemplos tienes disponible el código de dicha función.

Lo segundo que hacemos en la aplicación es crear una instancia de Teclado y de Personaje ya que son los dos elementos que vamos a utilizar para simular el autómata. Veremos las modificaciones que hemos tenido que hacer en la clase Personaje utilizada hasta ahora para que acepte animaciones ya que en los demás ejemplos en los que hemos usado esta clase la hemos inicializado con imágenes estáticas.

Antes de entrar en el bucle o game loop del juego declaramos un número considerable de variables auxiliares que vamos a tener que usar con diferentes propósitos. Te preguntarás, ¿cómo controlamos la temporización?

Lo más lógico es procesar la temporización en la clase Personaje donde se realice la animación del mismo. Esto nos permite personalizar la velocidad de cada personaje. Cuando desarrollemos un videojuego tendremos una clase Participante de la que heredarán varias clases hijas definiendo así los personajes principales, los adversarios y otros elementos. La temporización no podrá ser controlada de manera global a todas las instancias de las clases ya que esto limitaría la configuración del comportamiento. Necesitamos poder definir comportamientos diferentes para cada uno de los integrantes de la aplicación.

Para no complicar más el ejercicio hemos decidio utilizar un control de la temporización de manera local al personaje definiendo su comportamiento en el constructor para cada una de las acciones que realiza. En este ejemplo vamos a centrarnos en la implementación "informal" del autómata pero es interesante ver como hemos controlado el tiempo. Para controlar el tiempo hemos modificado el método paso_a_paso() perteneciente a la clase Animación. La nueva implementación es la siguiente:

<cpp> // Esta función lleva el control del tiempo

void Animacion::paso_a_paso(SDL_Surface *pantalla, int x, int y, int flip) {

   Uint32 t0 = SDL_GetTicks();
   Uint32 t1 = SDL_GetTicks();
  
   if(control_animacion->numero_cuadros() < 2) {

imagen->dibujar(pantalla, control_animacion->cuadro(), x, y, flip); control_animacion->avanzar();

   } else {

do {

t1 = SDL_GetTicks();

} while((t1 - t0) < retardo_);

   imagen->dibujar(pantalla, control_animacion->cuadro(), x, y, flip);
   control_animacion->avanzar();   
   }

} </cpp>


El código es bastante simple. Si tenemos más de un cuadro es que estamos en ante una animación y no una imagen fija. En este caso mediante un do while dejamos pasar el tiempo hasta que pase la cantidad de tiempo que hemos definido como retardo. Una vez haya cumplido este tiempo realizamos las acciones habituales de avanzar la posición del personaje y dibujar el frame actual.

Seguidamente actualizaremos el estado del teclado y tomaremos una "foto" del momento actual del personaje, tanto de su posición actual como del estado en el que se encuentra. Esto nos permitirá determinar si tenemos que realizar un repintado del personaje en pantalla. Siempre que el personaje no varíe de estado o de posición no se realizará dicho repintado.

Después de la inicialización de estas variables tenemos la lógica que pertenece a la implementación del autómata. Hemos usado una estructura selectiva diferente al switch - case pero igualmente válida. La lógica es la siguiente: se estudia cada uno de las acciones que puede producir el usuario sobre el teclado y se reacciona según sea la acción realizada. Por ejemplo, si pulsamos la tecla definida para andar hacia la derecha precesaremos dicha orden moviendo el personaje hacia la derecha cambiando su estado y su posición.

Esta es una manera intuitiva de implementar el autómatana pero no es la más correcta. El método de los cases se basa en el estudio del estado actual del personaje y no en el de las acciones que produzcan transiciones ya que necesitamos saber en qué estado estamos para reaccionar de una manera o de otra. Vamos a abordar la implementación correcta para el autómata.

Entonces, ¿cómo se construye el método de los cases? Lo primero que tenemos que hacer es construir, con una estructura selectiva, una esquema que nos permita tratar todos los posibles estados en los que pueda estar nuestro personaje. En nuestro caso se trata de una estructura con la siguiente forma:

<cpp> switch(estado) {

case PARADO:
    break;
    
case PARADO_DERECHA:
    break;
    
case PARADO_IZQUIERDA:
    break;
    
case SALTAR_DERECHA:
    break;
    
...
    
case AGACHAR_IZQUIERDA:
    break;
    
default:
    

}

</cpp>

Una vez tengamos codificada esta estructura tenemos que añadirle comportamiento. ¿Cómo hacemos esto? En este momento deberíamos de hacer una tabla con todos los posibles estados y las transiciones entre ellos para estudiar los posibles casos de aceptación del autómata. Como en este caso el autómata no es una máquina verificadora si no que es una herramienta para implementar el comportamiento de un personaje vamos a realizar el estudio caso por caso fijándonos en el diagrama de transiciones que expusimos al principio de la sección.

Vamos a empezar estudiando el caso Parado. Cuando nos encontremos en este estado pueden ocurrir dos acciones válidas: que pulsemos la tecla definida para realizar un movimiento a la derecha o que pulsemos la tecla con el mismo objetivo pero hacia la izquierda. Si pulsamos la tecla definida para el movimiento a la derecha el personaje cambiará de estado a Parado derecha y si pulsamos la tecla de movimiento hacia la izquierda pasará al estado Parado izquierda. Si no pulsamos ninguna tecla se quedará como estaba. Fácil ¿no?. Ahora tenemos que incluir este estudio en nuestra estructura selectiva. Nos vamos al caso donde estudiamos este estado e incluimos la lógica que nos permite simular el comportamiento que acabamos de describir. El resultado es el siguiente:

<cpp>

case PARADO:
   if(teclado.pulso(Teclado::TECLA_DERECHA))

// Si "->" cambio de estado a parado derecha

principal.cambio_estado(PARADO_DERECHA);

   else if(teclado.pulso(Teclado::TECLA_IZQUIERDA))

// Si "<-" cambio de estado a parado izquierda

principal.cambio_estado(PARADO_IZQUIERDA);


break;

</cpp>

Como puedes ver no es algo complicado. De manera análoga completamos los demás casos. El resultado del estudio del diagrama de transiciones es el siguiente:

<cpp> switch(principal.estado_actual()) {

case PARADO:
    
    if(teclado.pulso(Teclado::TECLA_DERECHA))

// Si "->" cambio de estado a parado derecha

principal.cambio_estado(PARADO_DERECHA);

    else if(teclado.pulso(Teclado::TECLA_IZQUIERDA))

// Si "<-" cambio de estado a parado izquierda

principal.cambio_estado(PARADO_IZQUIERDA);

    break;
    
case PARADO_DERECHA:
    
    if(teclado.pulso(Teclado::TECLA_SUBIR))

principal.cambio_estado(SALTAR_DERECHA);

    else if(teclado.pulso(Teclado::TECLA_DERECHA)) {

principal.avanzar_x(); principal.cambio_estado(ANDAR_DERECHA);

    } else if(teclado.pulso(Teclado::TECLA_DISPARAR))

principal.cambio_estado(GOLPEAR_DERECHA);

    else if(teclado.pulso(Teclado::TECLA_BAJAR))

principal.cambio_estado(AGACHAR_DERECHA);

    else

principal.cambio_estado(PARADO);


    break;
    
case PARADO_IZQUIERDA:
    
    if(teclado.pulso(Teclado::TECLA_SUBIR))

principal.cambio_estado(SALTAR_IZQUIERDA);

    else if(teclado.pulso(Teclado::TECLA_IZQUIERDA)) {

principal.retroceder_x(); principal.cambio_estado(ANDAR_IZQUIERDA);

    } else if(teclado.pulso(Teclado::TECLA_DISPARAR))

principal.cambio_estado(GOLPEAR_IZQUIERDA);

    else if(teclado.pulso(Teclado::TECLA_BAJAR))

principal.cambio_estado(AGACHAR_IZQUIERDA);

    else

principal.cambio_estado(PARADO);


    break;		
    
    
case SALTAR_DERECHA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case SALTAR_IZQUIERDA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case ANDAR_DERECHA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case ANDAR_IZQUIERDA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case GOLPEAR_DERECHA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
    
case GOLPEAR_IZQUIERDA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case AGACHAR_DERECHA:
    
    principal.cambio_estado(PARADO);
    
    break;
    
case AGACHAR_IZQUIERDA:
    
    principal.cambio_estado(PARADO);		
    
    break;
    
default:
    cerr << "No se conoce este estado" << endl;
    
    
}

</cpp>

Como puedes ver la estrutura toma una dimensión importante. No hemos separado en funciones los comportamientos a realizar en cada uno de los estados para que se viese las analogías y diferencias entre ellos. Esta es una manera más correcta de implementar el autómata pero a la hora de programar videojuegos usaremos aquella que nos sea más útil en un momento dado. El que las transiciones del autómata sean controladas por eventos de teclado produce que en ocasiones necesitemos discriminar el comportamiento por la entrada que recibimos de usuario más que por el estado en el que se encuentre el personaje en un momento dado.

Puedes ver que al implementar el autómata más formalmente hemos hecho desaparecer apaños que necesitábamos en la anterior implementación, como el de control hacia que lado estaba mirando el personaje en un momento dado o el de iniciar la aplicación en vez de en el estado parado en el que posicionaba a nuestro personaje mirando hacia la izquierda.

Para poder implementar el comportamiento del autómata hemos tenido que completar la clase personaje con varios métodos que nos permiten controlar el estado del personaje así como asociar animaciones a cada uno de estos estados. La implementación realizada es muy concreta para este caso en particular. A la hora de desarrollar nuestro videojuego de ejemplo realizaremos una implementación más general que nos permita reutilizar la clase sea cual sea el personaje que queramos crear. Vamos a ver las novedades de la implementación de esta clase:

<cpp> // Listado: Personaje.h // // Clase Personaje

  1. ifndef _PERSONAJE_H_
  2. define _PERSONAJE_H_
  1. include <SDL/SDL.h>

class Animacion; // Declaración adelantada

// Enumerado con los posibles estados del personaje

enum estados_personaje {

   PARADO,
   PARADO_DERECHA,
   SALTAR_DERECHA,
   ANDAR_DERECHA,
   GOLPEAR_DERECHA,
   AGACHAR_DERECHA,
   PARADO_IZQUIERDA,
   SALTAR_IZQUIERDA,
   ANDAR_IZQUIERDA,
   GOLPEAR_IZQUIERDA,
   AGACHAR_IZQUIERDA,   
   MAX_ESTADOS

};

class Personaje {

public:
   // Constructor
   Personaje(void);


   // Consultoras
   int pos_x(void);
   int pos_y(void);
   estados_personaje estado_actual(void);
   void dibujar(SDL_Surface *pantalla);
   // Modificadoras
   void cambio_estado(estados_personaje status);
   
   void pos_x(int x);
   void pos_y(int y);
   // Modifica la posición del personaje con respecto al eje X
   void avanzar_x(void);
   void retrasar_x(void);
   // Modifica la posición del personaje con respecto al eje Y
   void bajar_y(void);
   void subir_y(void);
  


private:
   
   // Posición
   int x, y;
   estados_personaje estado;
   // Galería de animaciones
   Animacion *galeria_animaciones[MAX_ESTADOS];
   

};

  1. endif

</cpp>

Lo primero que hemos incluido en la definición de la clase es un conjunto de estados del personaje. Asociados a estos estados tenemos dos funciones miembro. Una nos permite cambiar el estado del personaje mientras que la otra nos devuelve el estado actual del mismo.

En la parte privada de la clase hemos incluido una variable que controlará el estado actual del personaje y una galería de animaciones. Esta galería de animaciones tendrá el tamaño del número de estados disponible y asociará a cada estado una imagen o animación que reproducir cuando el personaje esté en dicho estado.

Veamos la nueva implementación de la clase:

<cpp> // Listado: Personaje.cpp // // Implementación de la clase Personaje

  1. include <iostream>
  2. include <SDL/SDL_image.h>
  1. include "Personaje.h"
  2. include "Animacion.h"

using namespace std;


// Constructor

Personaje::Personaje(void) {

   // Inicializamos las variables
   this->x = 0;
   this->y = 380;
   estado = PARADO_DERECHA;
   galeria_animaciones[PARADO_DERECHA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "0", 0);

   galeria_animaciones[SALTAR_DERECHA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "21", 0);

   galeria_animaciones[ANDAR_DERECHA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7,\ "1,2,3,2,1,0,4,5,6,5,4,0", 18);

   galeria_animaciones[GOLPEAR_DERECHA] = 

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "15", 0);

   galeria_animaciones[AGACHAR_DERECHA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "17", 2);

   galeria_animaciones[PARADO_IZQUIERDA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "7", 0);

   galeria_animaciones[SALTAR_IZQUIERDA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "20", 0);

   galeria_animaciones[ANDAR_IZQUIERDA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "8,9,10,9,8,7,11,12,13,12,11,7", 18);

   galeria_animaciones[GOLPEAR_IZQUIERDA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "14", 0);

   galeria_animaciones[AGACHAR_IZQUIERDA] =

new Animacion("./Imagenes/jacinto.bmp", 4, 7, "16", 2);


}


// Consultoras

int Personaje::pos_x(void) {

   return x;

}

int Personaje::pos_y(void) {

   return y;

}

estados_personaje Personaje::estado_actual(void) {

   return estado;

}

void Personaje::dibujar(SDL_Surface *pantalla) {

   // Según sea el estado del personaje
   // en un momento determinado 
   // dibujaremos una animación u otra
   
   switch(estado) {
    case PARADO:

estado = PARADO_DERECHA;

    case PARADO_DERECHA:

galeria_animaciones[PARADO_DERECHA]->paso_a_paso(pantalla, x, y); break;

   case SALTAR_DERECHA:

galeria_animaciones[SALTAR_DERECHA]->paso_a_paso(pantalla, x, y); break;

   case ANDAR_DERECHA:

galeria_animaciones[ANDAR_DERECHA]->paso_a_paso(pantalla, x, y); break;

   case GOLPEAR_DERECHA:

galeria_animaciones[GOLPEAR_DERECHA]->paso_a_paso(pantalla, x, y); break;

   case AGACHAR_DERECHA:

galeria_animaciones[AGACHAR_DERECHA]->paso_a_paso(pantalla, x, y); break;

   case PARADO_IZQUIERDA:

galeria_animaciones[PARADO_IZQUIERDA]->paso_a_paso(pantalla, x, y); break;

   case SALTAR_IZQUIERDA:

galeria_animaciones[SALTAR_IZQUIERDA]->paso_a_paso(pantalla, x, y); break;

   case ANDAR_IZQUIERDA:

galeria_animaciones[ANDAR_IZQUIERDA]->paso_a_paso(pantalla, x, y); break;

   case GOLPEAR_IZQUIERDA:

galeria_animaciones[GOLPEAR_IZQUIERDA]->paso_a_paso(pantalla, x, y); break;

   case AGACHAR_IZQUIERDA:

galeria_animaciones[AGACHAR_IZQUIERDA]->paso_a_paso(pantalla, x, y); break;

   default:

cerr << "Personaje::dibujar() " << endl;

   } // end switch(estado)


    SDL_Rect rect;
    rect.x = x;
    rect.y = y;
    SDL_BlitSurface(NULL, NULL, pantalla, &rect);
   

}


// Modificadoras

void Personaje::cambio_estado(estados_personaje status) {

   estado = status;

}

void Personaje::pos_x(int x) {

   this->x = x;

}


void Personaje::pos_y(int y) {

   this->y = y;

}

// El movimiento de la imagen se establece // de 4 en 4 píxeles

void Personaje::avanzar_x(void) {

   x += 6;

}

void Personaje::retrasar_x(void) {

   x -= 6;

}

void Personaje::bajar_y(void) {

   y += 4;

}

void Personaje::subir_y(void) {

   y -= 4;

} </cpp>

En el constructor de la clase hemos añadido la incialización de las nuevas variables que contiene la clase. En la galería de animaciones hemos asociado las animaciones a los estados haciendo uso de la clase Animacion para manejarlas. En cuanto a las demás funciones hay pocas diferencias con respecto a la clase personaje antes implementada. La principal es la nueva función dibujar que muestra el frame o cuadro perteneciente a la animación asociada al estado actual del personaje.

Como puedes ver en el programa principal hemos hecho uso de una pequeña librería que hemos creado para la inicialización de SDL. No es más que la agrupación funcional de las tareas que debemos realizar al inicio con SDL por lo que puedes consultarla en los listados.

[editar] Conclusión

La mayor parte del trabajo dedicado a construir un personaje consiste en diseñar un autómata que responda correctamente a los comportamientos que queramos que tenga nuestro personaje. La implementación con o sin SDL es bastante sencilla y fácil de modificar. Es importante que le dediques una buena parte del tiempo al diseño lo que te ahorrará tener que modificar codificación cuando estés inmerso en el desarrollo del proyecto.


[editar] Colisiones

[editar] Introducción

¿Qué sería de un videojuego dónde el personaje principal no pudiese realizar más acciones que su propio movimiento? La respuesta es rápida: Nada. La interacción de un personaje con los elementos que componen la escena de un videojuego es fundamental. Cuando un personaje se encuentra con otro debemos de tener bien definidas las acciones que queramos que ocurran en respuesta a dicha "colisión".

Por ejemplo si nuestro personaje se encuentra con un malvado villano y este nos alcanza lo más lógico es que hagamos descender nuestro nivel de vida. Si pasamos sobre un objeto tendremos que ser capaces de cogerlo o realizar alguna acción con él. De esto tratan las colisiones.

[editar] ¿Qué es una colisión? Detectando colisiones

Podemoso definir una colisión entre dos elementos como un choque entre estos dos elementos. En el mundo del videojuego el significado de esta palabra es el mismo añadiéndole algunos matices. Se produce una colisión cuando dos personajes u objetos del videojuego chocan entre ellas.

Ejemplo de colisión.
Ejemplo de colisión.

Dos personajes chocan entre ellos lógicamente, lo que se produce en realidad en SDL, a un nivel físico, es el solapamiento de superficies. En SDL, como recordarás, las superficies son unos elementos rectángulares donde tenemos una determinada información gráfica. Estas superficies tienen una posición determinada por su esquina superior derecha, una altura y una anchura determinada.

Dos superficies habrán colisionado cuando algún píxel de alguna de ellas esté contenido dentro de un píxel de la otra superficie. No es un concepto complicado pero conlleva una serie de problemas. El primero es que la gran mayoría de personajes y elementos no son rectángulares. Cuando dibujamos a los integrantes de nuestra aplicación lo haremos sobre un lienzo de un color que habremos escogido como color clave para que no se reproduzca durante el blitting. El tamaño de este lienzo, que también es rectángular, es mayor que el propio elemento. Al cargar esta imagen en pantalla cargamos el rectángulo completo. Esto provoca que se detecte la colisión antes de que se produzca por el exceso de tamaño del lienzo con respecto al personaje que tenemos dibujado en él.

Elementos de una imagen SDL.
Elementos de una imagen SDL.

La detección de colisiones es una técnica que consiste en implementar unas funciones que nos permitan conocer cuando ha existido una colisión entre dos elementos. Este va a ser el núcleo de nuestro trabajo en cuanto a las colisiones.

Existen varias formas de solventar en cierta medida los problemas que acarrea el que las superficies sean rectángulares. La primera de ellas es recortar todo lo posible el lienzo sobrante así tendremos menos exceso por lo que el comportamiento de la colisión será más adecuado. Esto muchas veces no está en nuestras manos. Como programadores, y en proyectos de cierta envergadura, no nos haremos cargo del diseño de los personajes y seguramente no tengamos tiempo de ajustar todos los lienzos que pueden existir en una animación con el trabajo que supone retocar una rejilla de imágenes.

La segunda forma de evitar un mal comportamiento de las colisiones es realizar una buena implementación de las funciones dedicadas a detectar dichas colisiones. Podemos añadir información a nuestro personaje que ciña la superficie de colisión lo más posible al personaje para conseguir una mejor respuesta. Esta es la opción más pausible y la que vamos a desarrollar.

En definitiva, una colisión es un solapamiento de píxeles al que, normalmente, deberemos de reaccionar. Para que esta reacción se haga en el momento correcto tendremos que implementar unas funciones dedicadas a la detección de colisiones que veremos a continuación.


[editar] Tipos de colisiones

Existen varias clasificaciones para las colisiones según sobre que aspecto queramos discriminar las diferencias entre unas y otras. La primera y más común es la clasificación que diferencia entre el tipo de elemento con el que se produce la colisión. Según dicha clasificación las colisiones pueden ser:

  • Entre personajes: Estas colisiones se producen entre los elementos "activos de la aplicación. Cuando vamos por el mapa de nuestro juego y el personaje principal se encuentra con un enemigo y aproximan lo suficiente se produce una colisión de este tipo. De la misma manera que si nuestro personaje principal tiene alguna clase de arma y dispara cuando dicho disparo llegue al enemigo este producirá una colisión con el adversario.
  • Con el escenario: Este tipo de colisiones son el que se producen entre el personaje y los elementos que componen el escenario o el nivel en cuestión. En un videojuego de plataformas cada una de las superficies donde puede subirse nuestro personaje y el propio personaje producen una colisión que provoca que podamos mantenerlo subida en ella. Si no se produjese dicha colisión tendríamos que usar otra técnica para que nuestro programa supises que dicha plataforma es suelo y debe interpretarlo como tal.


También podemos clasificar las colisiones según su forma. Existen colisiones de un cuerpo que son aquellas en las que se utiliza una forma geométrica para representar al personaje o al objeto en cuestión para realizar la detección de colisiones y las colisiones compuestas que son aquellas en las que los integrantes que van a colisionar han sido divididos en varios rectángulos que nos van a permitir diferenciar la figura real del personaje. En cuantas más figuras geométricas dividamos a nuestro personaje mejor comportamiento obtendremos de las colisiones.

Las clasificaciones de las colisiones son un aspecto meramente informativo que nos van a ayudar a plantear la detección de colisiones para determinar de qué manera actuar según el caso que nos encontremos.


[editar] Implementando la detección de colisiones

Vamos a implementar diferentes tipos de detección de colisiones desde los métodos más sencillos hasta algunos más complejos. Es importante saber diferenciar cuando usar un método de detección de colisiones u otro. El compromiso entre tiempo de procesamiento y resultado tiene que estar siempre presente.

Después de estudiar este apartado vas a ser capaz de crear tu propio método de detección de colisiones. Imagina que tienes un ítem que es una pequeña esfera que le va a proporcionar puntos a tu personaje principal. Esta esfera está almacenada en una imagen cuadrada. Ahora tienes varias opciones, la primera es implementar la colisión tomando como superficie el cuadrado que envuelve a la esfera. Seguramente si la esfera es pequeña en el transcurso de la partida no notarás que la colisión no se produce al cien por cien del acercamiento.

La otra alternativa es implementar una función que divida la esfera en cien pequeños rectángulos parelelos horizontalmente. Cada vez que se produzca una vuelta en el game loop habrá que comprobar si existe colisión con este elemento. Esto provocará un fuerte aumento de los recursos para obtener una respuesta que casi teníamos con el método anteriormente descrito. Estas son decisiones de diseño que debes de tomar a la hora de crear un videojuego.

Vamos a empezar con una implementación de las colisiones sencilla.


[editar] Detección de colisiones de superficie única

Existen varios casos de detección de colisiones de superficies únicas. El primero es el que hemos planteado en esta sección como paradigma del mal que en ocasiones puede resultar ventajoso. Se trata de utilizar como superficie de colisión el rectángulo que envuelve a nuestros personajes.

Vamos a realizar una primera versión de lo que sería la implementación del método de las colisiones. Vamos a tratar las colisiones que se producen entre las superficies sin considerar que parte de ellas están o no dibujadas. Este método es válido cuando utilicemos dibujos que rellenen toda la superficie donde han sido almacenados.

Vamos a tomar de referencia nuestra clase personaje sin animaciones asociadas para simplificar todo lo posible el código. A esta clase vamos a añadirle dos nuevos métodos ancho() y alto() que nos permiten conocer el ancho y alto de la imagen que va a almacenar como represetación del personaje, o lo que es lo mismo, la superficie que cumple con este objetivo. Estos métodos nos van a ayudar a la hora de realizar el cálculo de la colisión. Además hemos comentado la parte de la clase donde se establecia el colo clave para se pueda observar cuando entran en contacto las superficies.

La implementación de la función que nos permite calcular la colisión es la siguiente:

<cpp> // Listados: Colisiones.cpp // // Implementación de funciones dedicadas a la detección // de colisiones

// Esta función devuelve true si existe colisión entre los dos // personajes que recibe como parámetros

  1. include <iostream>
  2. include "Personaje.h"
  3. include "Colision_superficie.h"

using namespace std;


bool colision_superficie(Personaje &uno, Personaje &otro) {

   int w1, h1, w2, h2, x1, y1, x2, y2;
   
   w1 = uno.ancho();
   h1 = uno.alto();
   w2 = otro.ancho();
   h2 = otro.alto();
   
   x1 = uno.pos_x();
   y1 = uno.pos_y();
   
   x2 = otro.pos_x();
   y2 = otro.pos_y();
   
   if( ((x1 + w1) > x2) &&

((y1 + h1) > y2) && ((x2 + w2) > x1) && ((y2 + h2) > y1))

return true;


   return false;

} </cpp>

Para que sea más fácil de entender hemos definido unas variables enteras para almacenar tanto la posición como el ancho y alto de los dos personajes de los que tenemos que comprobar si existe colisión o no. La técnica es la siguiente. Se trata de comprobar si alguno de los puntos de una superficie están contenidos en la otra.

Con x1 y w1 tenemos cubierto la parte horizontal del primer personaje. Para que exista colisión x2, que es la posición en x del personaje otro, tiene que ser menor que la suma de x1 y w1. Este razonamiento lo extendemos a cada uno de los lados de las dos superficies y tenemos la función que detecta las colisiones.

La función, como no podía ser de otra forma, devolverá true} en caso de existir colisión y false} en caso de que no exista. En el programa de ejemplo hemos hecho uso de la librería adicional SDL_ttf} para mostrar un mensaje cada vez que exista colisión.

Al ejecutar la aplicación puedes ver como la colisión se produce mucho antes de que los personajes se encuentren en realidad. Esto hace que el comportamiento de esta implementación no sea aceptable en muchos casos. Vamos a mejorar esta implementación.

Vamos a proponer una nueva solución que nos permita seguir utilizando un sólo rectángulo por superficie pero que tenga un mejor comportamiento. Se trata de añadir información en la clase personaje de manera que especifiquemos mediante una posición inicial, altura y anchura el rectángulo de la superficie que ocupa el personaje en realidad.

La implementación de la detección de colisiones no variará más que en cambiar las variables de estudio de cada uno de los personajes. Vamos a ver los cambios en la clase personaje:

<cpp> // Listado: Personaje.h // // Clase Personaje complementada para la detección de colisiones

  1. ifndef _PERSONAJE_H_
  2. define _PERSONAJE_H_
  1. include <SDL/SDL.h>


class Personaje {

public:
   // Constructor
   Personaje(char *ruta, SDL_Rect &real, int x = 0, int y = 0);


   // Consultoras
   int pos_x(void);
   int pos_y(void);
   int ancho(void);
   int alto(void);
   int pos_x_real(void);
   int pos_y_real(void);
   int ancho_real(void);
   int alto_real(void);
   void dibujar(SDL_Surface *pantalla);
   // Modificadoras
   void pos_x(int x);
   void pos_y(int y);
   // Modifica la posición del personaje con respecto al eje X
   void avanzar_x(void);
   void retrasar_x(void);
   // Modifica la posición del personaje con respecto al eje Y
   void bajar_y(void);
   void subir_y(void);
  


private:
   
   // Posición
   int x, y;
   
   SDL_Rect superficie_real;
   SDL_Surface *imagen;

};

  1. endif

</cpp>

Como puedes ver hemos incluido una nueva variable de tipo rectángulo en la parte privada de la clase que controlará que área dentro de la superficie de la imagen, o de la animación que tenga asociada, es válida para la realizar la detección de colisiones, es decir, donde está dibujado realmente el personaje. En el caso de tener una animación y existir una diferencia notable entre la parte de la superficie que se encuentra el personaje entre un cuadro y otro tendríamos que definir una secuencia que según el paso en el que se encontrase la animación utilizase un cuadro u otro para el cálculo de la colisión.

Además hemos incluido en la clase cuatro nuevos métodos que nos van a permitir conocer la posición real del personaje en pantalla, así como su anchura altura. Como ya hemos dicho lo más probable es que el personaje no ocupe toda la superficie donde está almacenado y estas funciones nos permiten saber donde se encuentra la imagen actual del personaje. La implementación de dichas funciones es:

<cpp> // Funciones para la implementación de la detección de colisiones

int Personaje::pos_x_real(void) {

   // Desplazamiento con respecto a la posición actual
   return superficie_real.x + x;

}

int Personaje::pos_y_real(void) {

   return superficie_real.y + y;

}

// Alto y ancho ajustados al personaje

int Personaje::ancho_real(void) {

   return superficie_real.w;

}

int Personaje::alto_real(void) {

   return superficie_real.h;

} </cpp>

Como puedes ver estas funciones calculan el desplazamiento interno que produce que el personaje no ocupe toda la superficie donde es almacenado. En cuanto a la nueva función que calcula la colisión es muy parecida a la primera que hemos visto pero esta vez usa las funciones que proporcionan las dimensiones reales del personaje. Aquí tienes la nueva implementación:

<cpp> // Listados: Colisiones.cpp // // Implementación de funciones dedicadas a la detección // de colisiones

// Esta función devuelve true si existe colisión entre los dos // personajes que recibe como parámetros // // Superficies únicas ajustada

  1. include <iostream>
  2. include "Personaje.h"
  3. include "Colision_superficie_ajustada.h"

using namespace std;

bool colision_superficie_ajustada(Personaje &uno, Personaje &otro) {

   int w1, h1, w2, h2, x1, y1, x2, y2;
   
   w1 = uno.ancho_real();
   h1 = uno.alto_real();
   w2 = otro.ancho_real();
   h2 = otro.alto_real();
   
   x1 = uno.pos_x_real();
   y1 = uno.pos_y_real();
   
   x2 = otro.pos_x_real();
   y2 = otro.pos_y_real();
   
   if( ((x1 + w1) > x2) &&

((y1 + h1) > y2) && ((x2 + w2) > x1) && ((y2 + h2) > y1))

return true;


   return false;

} </cpp>

Como puedes ver al ejecutar el programa de ejemplo hemos conseguido una respuesta bastante ajustada de la colisión, podríamos decir que casi milimétrica. En muchos casos no nos interesará hacer una implementación pormenorizada de la colisión si no que será suficiente con un comportamiento de este estilo que no sobrecarge al sistema. Lo comentábamos al comienzo de la sección. Se trata de establecer una línea bien definida en el compromiso rendimiento/respuesta.

Vamos a estudiar otros métodos para la detección de colisiones.


[editar] Detección de colisiones de superficies compuestas

Hemos conseguido una buena implementación para calcular las colisiones entre dos de nuestros personajes. Pero que ocurriría si nuestros personajes tuviesen forma triangular. No habría forma de aproximar la forma del rectángulo a dicho triángulo. Vamos a utilizar el ejemplo anterior con dos triángulos para ver como respondería la implementación de las colisiones.

Colisión simple entre triángulos
Colisión simple entre triángulos

En el ejemplo 10 hemos utilizado el mismo código que en el primer caso para detectar las colisiones. Se puede observar como el resultado no es aceptable. Los dos triángulos no se llegan ni a acercar cuando la función informa ya de una colisión. Tenemos que buscar una solución.

En el apartado anterior ajustamos el rectángulo a la figura lo que nos proporcionó una solución válida. En este caso no vale. No podemos ajustar un sólo rectángulo a la figura de un triángulo. Existen varias posibles soluciones para este problema.

La primera que vamos a afrontar es un método generalista que nos va a permitir solucionar mucho de los problemas que genera la detección de colisiones. Se trata de dividir la figura que queramos "colisionar" en rectángulos lo suficientemente pequeños como para que la respuesta del sistema sea válida. Es importante que dichos recuadros tengan un tamaño adecuado ya que es un problema si son excesivamente pequeños o excesivamente grandes.

Cuando comprobemos una colisión por este método tendremos que estudiar cada uno de los rectángulos que componen las figuras que colisionan. Si los rectángulos en los que hemos divido la figura son demasiado pequeños la carga del sistema será alta, muchas veces, sin necesidad. Sin embargo si no tienen un tamaño lo suficentemente pequeño puede ser que no consigamos el comportamiento deseado.

Para la implementación de este nuevo método necesitamos ampliar la clase Personaje para que nos ofrezca suficiente información sobre la composición de la superficie que lo representa. En un vector vamos a almacenar los rectángulos que van a componer la superficie. Además añadimos un método que nos permite añadir elementos a dicho vector.

Triángulo dividido en rectángulos
Triángulo dividido en rectángulos

En el programa principal tenemos que construir las superficies de colisión a partir de la imagen que hemos divido en rectángulos para poder utilizar la función que nos permite detectar las colisiones haciendo uso de dicho método. El código resultante es el siguiente:

<cpp>

   // Vamos a contruir el área de colisión a partir de rectángulos
   // para el personaje principal
   SDL_Rect rect_principal[12] = {{3, 82, 92, 8},

{10, 75, 78, 6}, {14, 67, 71, 7}, {17, 60, 64, 6}, {26, 45, 48, 5}, {29, 39, 40, 5}, {32, 34, 34, 4}, {35, 29, 28, 4}, {38, 24, 22, 4}, {40, 18, 18, 5}, {44, 13, 11, 4}, {47, 8, 5, 4}};


   for(int i = 0; i < 12; i++)

principal.annadir_rectangulo(rect_principal[i]);


   // Vamos a contruir el área de colisión a partir de rectángulos
   // para el adversario
   SDL_Rect rect_adversario[12] = {{82, 3, 8, 92},

{75, 10, 6, 78}, {67, 14, 7, 71}, {60, 17, 6, 64}, {45, 26, 5, 48}, {39, 29, 5, 40}, {34, 32, 4, 34}, {29, 35, 4, 28}, {24, 38, 4, 22}, {18, 40, 5, 18}, {13, 44, 4, 11}, {8, 47, 4, 5}};


   for(int i = 0; i < 12; i++)

adversario.annadir_rectangulo(rect_adversario[i]); </cpp>

Ahora vamos con lo que más nos interesa. ¿Cómo detectamos una colisión entre dos personajes que están compuesto por rectángulos? La base del razonamiento parte del primer ejemplo de detección de colisiones. En ese caso comprobabámos si algún punto de la superficie de uno de los personajes estaba contenido en la superifice del otro personaje. Para este caso vamos a extender el razonamiento. Tenemos dos superficies cuyas áreas de colisión están compuestas por múltiples rectángulos para saber si existe una colisión basta con comprobar si algún punto de alguna de las áreas que compone cada personaje está contenida en alguna de las superficies que componene al otro personaje.

Para realizar esta comprobación tenemos que anidar dos bucles que nos permitan recorrer por cada una de las superficies del primer personaje todas las del segundo participante en la posible colisión. El resultado de la implementación de este método es el siguiente:

<cpp> // Listado: Colisiones.cpp // // Implementación de funciones dedicadas a la detección // de colisiones

// Esta función devuelve true si existe colisión entre los dos // personajes que recibe como parámetros. // // Estos personajes estarán compuestos por rectángulos

  1. include <iostream>
  2. include "Personaje.h"
  3. include "Colisiones.h"

using namespace std;


// Colisión entre dos personajes divididos en rectángulos // Devuelve true en caso de colisión // O(n^2)


bool colision(Personaje &uno, Personaje &otro) {

   int w1, h1, w2, h2, x1, y1, x2, y2;
   // Todos los rectángulos del primer personaje
   for(size_t i = 0; i < uno.rectangulos.size(); i++) {
   
    

w1 = uno.rectangulos[i].w; h1 = uno.rectangulos[i].h; x1 = uno.rectangulos[i].x + uno.pos_x(); y1 = uno.rectangulos[i].y + uno.pos_y();


// Con todos los rectángulos del segundo personaje

for(size_t j = 0; j < otro.rectangulos.size(); j++) {

w2 = otro.rectangulos[j].w; h2 = otro.rectangulos[j].h; x2 = otro.rectangulos[j].x + otro.pos_x(); y2 = otro.rectangulos[j].y + otro.pos_y();


// Si existe colisión entre alguno de los // rectángulos paramos los bucles

if( ((x1 + w1) > x2) && ((y1 + h1) > y2) && ((x2 + w2) > x1) && ((y2 + h2) > y1))

return true; }

   }


   return false;

} </cpp>

Vemos en la implementación como hemos usado dos bucles for para resolver la detección de colisiones. En el más externo vamos recorriendo los recuadros que componen el primer personaje obteniendo la posición de cada uno de ellos en las variables (x1, y1), mientras que en las variables w1 y h1 almacenamos el tamaño del recuadro en cuestión. Para cada uno de estos rectángulos recorremos cada uno de las superficies que componen al otro personaje en el bucle más interno. Utilizamos unas variables análogas de las que utilizamos para el primer personaje pero con el identificativo de 2. Por cada par de rectángulos realizamos una comprobación por si existe algún punto de uno de ellos que esté contenido en el otro. Si es así devolvemos el valor true para indicar que ha existido una colisión. En caso de recorrer todos los rectángulos sin que exista contención en ninguno de los sentidos devolvemos el valor false}.

Con esta técnica podemos programar colisiones para cualquier tipo de superficies. Basta con dividir dicha superficie en rectángulos suficientes para tener un buen comportamiento. Vamos a realizar dos ejercicios que nos van a permitir prácticar un poco con esta técnica.

[editar] Ejercicio 2

Utiliza la técnica que acabamos de estudiar para comprobar la colisión de dos esferas. Vamos a utilizar las clases implementadas en el último ejemplo. El trabajo de este ejercicio consiste en dividir la esfera en un número de partes racional de la mejor manera posible que nos ofrezca una comportamiento correcto en la detección de colisiones.

Puedes ver la solución en los listados de este tema en la carpeta ejercicio 2. La solución se centra en la división de la esfera. En este caso:

Esfera dividida en rectángulos
Esfera dividida en rectángulos

<cpp>

   // Vamos a contruir el área de colisión a partir de rectángulos
   // para el personaje principal
   SDL_Rect rect_principal[9] = {{22, 22, 107, 105},

{33, 12, 84, 9}, {42, 1, 66, 10}, {12, 33, 9, 84}, {1, 41, 10, 66}, {32, 128, 84, 9}, {41, 138, 66, 10}, {128, 33, 9, 84}, {138, 41, 10, 66}};


   for(int i = 0; i < 9; i++)

principal.annadir_rectangulo(rect_principal[i]); </cpp>


[editar] Ejercicio 3

Vamos a realizar la misma práctica del ejercicio anterior pero esta vez con nuestro personaje principal. Utiliza el método que estamos aplicando para simular el comportamiento del segundo método de detección de colisiones para delimitar el área de colisiones del rival.

Jacinto dividido en secciones
Jacinto dividido en secciones

El resultado completo lo tienes en el material del curso. Aquí puedes ver como hemos descompuesto al personaje principal.

<cpp>

   // Componemos los personajes
   // Vamos a contruir el área de colisión a partir de rectángulos
   // para el personaje principal
   SDL_Rect rect_principal[3] = {{27, 3, 41, 35},

{34, 39, 37, 32}, {30, 72, 46, 24}};


   for(int i = 0; i < 3; i++)

principal.annadir_rectangulo(rect_principal[i]);

</cpp>

¿Sencillo no? El trabajo ha consistido principalmente en medir que proporciones queremos para cada personaje y de cuantos rectángulos lo vamos a componer.

Como puedes ver en este caso la respuesta en parecida a la que habíamos conseguido ajustando el tamaño de la superficie al personaje por lo que hay que sopesar si nos interesa aumentar la carga del sistema en la detección de colisiones en este caso.

[editar] Otros algoritmos de detección de colisiones

El objetivo de este capítulo es que conozcas diferentes técnicas de implementación de colisiones. Existen numerosos artículos escritos sobre este tema que es cuestión de profundizar si necesitas otro tipo de respuesta.

Un algoritmo infalible es que calcula si un píxel cuando colisiona con otra superficie esta es de la parte. La carga que supone tener que hacer este cálculo por cada uno de los píxeles que colisionan ha hecho que la descartemos como opción viable. Se trata de, una vez detectada colisión entre los rectángulos comprobar si los píxeles que colisionan son parte del color key o no.

Sobre la detección de colisiones existen mumerosos artículos que te pueden ayudar a abrir la mente sobre este tema. Desde aquí te recomendamos un acercamiento a esta lectura que será muy enriquecedora para tu formación.

[editar] Recopilando

En este capítulo hemos tratado todo lo referente a los personajes y sprites que integrarán nuestro videojuego. Hemos visto como implementar los distintos tipo de animaciones de los participantes así como la manera de almacenar el contenido multimedia asociados a éstos en una galería.

Hemos presentado informalmente el concepto de autómata y su relación con los participantes del juego y con sus distintos tipos de animaciones.

Para terminar hemos estudiado el concepto de colisión así como diferentes técnicas para llevar a cabo su diseño y codificación.

Herramientas personales