Temario/TemaPRUEBAS

De Tutorial LibSDL

[editar] Un ejemplo de la creación de un videojuego

Tabla de contenidos


[editar] Introducción

Llegados a este punto es el momento de realizar un pequeño videojuego de ejemplo que nos sirva para acoplar e interrelacional todos los conocimientos vistos en este tutorial. Vamos a seguir la misma filosofía que hemos llevado durante todo el tutorial. Vamos a implementar el videojuego con un código lo más asequible posible y cercano al C.

Vamos usar algunos aspectos integrados en el lenguaje C++ por motivos de comodidad y eficiencia pero sólo usando elementos muy básicos que sean de rápida y fácil compresión. Entre ellos está el concepto de clase (y todo lo que supone esto) y algunas estructuras de la STL.

En este capítulo vamos a plantear la historia, vamos a crear los personajes, los niveles con su correspondiente editor de niveles y la justificación de todas las decisiones que hemos tomado para el desarrollo del videojuego.


[editar] Conocimientos previos

Para llevar a cabo este capítulo deberás haber estudiado todos los capítulos previos del tutorial para que el esfuerzo que tengas que hacer por aprovechar el contenido de éste sea razonable.

Además de los conocimientos previos de SDL debes de manejarte con cierta soltura en los aspectos básicos de C++ que hemos utilizado hasta la fecha ya que serán puestos en prácticas en este capítulos.

Vamos a realizar una pequeña introducción informal a la metodología UML que te será muy útil a la hora de plantearte el desarrollo de una aplicación con SDL.


[editar] Objetivos

Los objetivos de este capítulo son:

  1. Conocer y comprender como integrar las distintas partes de SDL para realizar un videojuego.
  2. Conseguir un acercamiento del lector al seguimiento de un patrón de diseño software como es UML.


[editar] Planteamiento informal de un videojuego

Vamos a tratar en primer lugar una pequeña introducción que nos va a permitir plantear informalmente un videojuego. Desde cómo elaborar una historia, pasando por los personajes, creación de niveles... Este proceso no difiere mucho en la mayoría de los casos de otros procesos de creación como el de una película o el relato de una novela complementados con el proceso de desarrollo software.

Una vez que hayamos realizado este recorrido por el planteamineto del videojuego vamos a proceder al análisis, diseño e implementación del mismo de una manera formal para que te acerques a los procesos y patrones formales necesarios para la creación de un juego y, probablemente, de cualquier aplicación.

[editar] La historia

[editar] Introducción. Cómo contar la historia

A la hora de crear un videojuego lo primero que tenemos que saber es qué historia queremos contar. Puede darse el caso de que queramos programar un videojuego que no necesite historia que lo envuelva y no necesitamos desarrollar este aspecto.

En cuanto al desarrollo de la historia no hay una metodología que nos permita crear historias claramente. Hay varios aspectos sobre la manera de escribir, hablar, mostrar cosas... que nos permiten provocar en el usuario tensión, felicidad, relax... Por ejemplo si queremos que una escena sea terrorífica de sentido común es oscurecer la escena, reproducir una música intrigamente al mismo tiempo que utilizamos frases cortas y contundentes en los textos que sean mostrados por pantalla.

Si por el contrario queremos que la escena sea alegre no hay nada como unos colores vivos, unos personajes felices y una música marchosa para conseguirlo. Como ya hemos dicho hay juegos que no necesitan de una historia para ser jugado. Estos juegos cuando son englobados en una trama son mucho más interesantes, veamos un ejemplo.

Imagina que has creado un videojuego del popular Sudoku y lo liberas. Si el videojuego es bueno puede que tenga una aceptación y que alguien afín al juego del Sudoku se haga con tu creación. Ahora imagina que has envuelto al Sudoku en una trabajada historia que empezara tal como así:

Al lejano oriente llegó un extranjero enamorado de una bella dama oriental.
Él la amaba pero su familia no lo aceptaba. Un buen día el patriarca decidió dar una oportunidad
a tan valiente foráneo. Te casarás con mi hija si superas los retos que te imponga

Como habrás podido suponer el reto será resolver varios Sudokus haciendo que la dificultad de los mismos sea gradual. Con este pequeño añadido al juego y una ambientación adecuada no sólo los amantes de los Sudokus valorarán tu obra si no que sumergerás en este mundo a otra clase de usuario no tan afín a este tipo de juegos. Esta táctica es muy utilizada actualmente para realizar remakes de juegos clásicos o de juegos de mesa transformados en videojuegos para complementarlos ofreciendo un mayor aliciente.

Una historia debe de tener unas partes bien definidas:


  • Situación: Es la introducción de la historia donde situamos al jugador en la época y le presentamos la historia en la que se va a envolver.
  • Desarrollo: Es la trama de la historia que debe ir avanzando paralelamente a los niveles que vaya completando nuestro jugador.
  • Desenlace: Es el final del juego. Existen numerosos artículos dedicados a la decepción que produce un videojuego con un mal desenlace. El jugador espera que después de haber dedicado parte de su tiempo a completar el videojuego el final de éste le proporcione un refuerzo positivo para aventurarse en un nuevo videojuego.


La calidad de la historia está en lo que tu imaginación sea capaz de producir. ¡Mucha suerte!


[editar] Nuestra historia

La historia de nuestro videojuego es muy simple:


Jacinto es un electricista especialista en montajes. Un buen día su jefe le encargó el mantenimiento del cableado estructurado de un edificio de oficinas. Jacinto estudió mucho para conocer todas las especificaciones y usos de estos cableados siendo el RJ 45 su mejor aliado.

No sabía Jacinto que su trabajo iba a ser tan duro. Veía como todos los días le desaparecían las herramientas y que los cables aparecían rotos todas las mañanas y era un gran enigma para él. Un buen día decidió pernoctar en la oficina. Su sorpresa fue cuando vio a unas pequeñas ratas que se hacían con todo el material. Le robaban destornilladores, alicates... todo lo que veían. Aliadas de las ratas las motas de polvo impedían que Jacinto recuperase su material. ¡¡Ayuda a Jacinto!! Elimina a todas las ratas y a sus amigas las motas de polvo y recupera el material o su jefe lo despedirá.


Como puedes ver es una trama muy simple y escueta que nos servirá como punto de partida para desarrollar el videojuego. Ya sabemos que vamos a tener un personaje principal, que seguramente el juego sea de plataformas y que habrá al menos dos tipos de enemigos: las ratas y las motas de polvo. Como puedes ver sentarte a crear una historia puede ayudarte a plantear el juego que vamos a crear.


[editar] Los personajes

Una vez desarrollada la historia del juego será mucho más fácil plantear los personajes. Nuestro personaje principal debe de cumplir unos requisitos que lo hagan especial para el desarrollo del juego. Esto lo hará mucho más atractivo.

Entre los personajes podemos distinguir a tres tipos: el personaje principal, los adversarios y los objetos o ítems. El personaje principal debe de poder distinguirse claramente de los demás y tener unas animaciones lo más detalladas posibles que nos permitan disfrutar de su creación. Deberemos de diseñar todos los movimientos que puede realizar tal como vimos en el capítulo dedicado a los personajes.

Adversarios debe haberlos de varios tipos. La dificutad con la que son eliminados debe de ser fácilmente reconocible según el aspecto que tengan. Si un enemigo va a ser difícil de eliminar debe de tener un aspecto robusto frente a otro que sea más fácil de hacer desaparecer. Si recuerdas algún juego de plataformas puedes ver como se pone de manifiesto este concepto.

Los objetos deben de ser fácilmente identificables y poseer alguna animación que los haga destacar del fondo del juego. Tendremos que decidir que nos aporta el obtener un objeto u otro.

En nuestro videojuego de ejemplo vamos a tener un personaje principal, nuestro electricista Jacinto. Las acciones y movimientos que puede realizar va a ser un subconjunto de las que diseñamos en el capítulo anterior que versaba sobre el diseño de personajes. Como enemigos tenemos a ratas y motas de polvo que tendrán un comportamiento similar y que deberemos de eliminar a base de golpes con el maletín.

Tendremos varios tipos de objetos que nos permitirán saltar, andar, navegar por el nivel y otros objetos o ítems que serán los que debamos ir recogiendo por el camino. Vamos a dejar como ejercicio el criterio por el que Jacinto podrá pasar de nivel. En unas líneas trataremos este tema.

[editar] Los niveles y el Editor

[editar] La creación de niveles

Depende del tipo de videojuego que queramos crear podremos diseñar los niveles de muy distinta forma. Si es un juego de lógica iremos complicando la estructura del juego hasta un nivel suficiente cada vez que pasemos un reto.

La creación de niveles tiene mucho contenido creativo como ocurría con el del desarrollo de la historia. Tenemos que diseñar niveles con distinto grado de dificultad que nos permitan ir avanzando por situaciones cada vez más difíciles que sortear. No existe una técnica definida para el diseño de estos niveles y tiene que ser nuestra capacidad de creación la que los determine.

En un nivel tendremos a nuestro personaje principal así como a los adversarios y los objetos. Para pasar de un nivel a otro deberemos de establecer unos objetivos que nos permitan propomocionar por los distintos niveles.

En nuestra aplicación Jacinto no tiene definido que debe hacer para pasar de nivel y es una tarea que propondremos como ejercicio. Jacinto va a ir recogiendo diferentes herramientas y eliminando enemigos. Una de estas dos acciones puede ser suficiente para establecer un criterio de promoción de nivel. Por ejemplo podemos hacer que el personaje pase al nivel siguiente una vez recoja todo el material o bien cuando haya eliminado a todos los enemigos.

Es importante medir bien la dificultad de los niveles. Si creamos un juego demasiado complicado producirá una fustración en el jugador que hará que pierda interés en seguir con el mismo. Todos los aficionados a los videojuegos recordamos algún nivel o algún juego que se nos atragantó y abandonamos al ser superados por el mismo.


[editar] El editor de niveles. Los tiles

El editor de niveles está intimamente ligado al tipo de videojuego para el que se implementa éste. Nosotros para crear los distintos niveles hemos desarrollado un editor. Este editor nos permite establecer la posición de todos los elementos que integran el nivel tanto decorados, personajes, ítems...

La técnica que sigue el editor para construir el nivel es muy utilizada en el mundo de los videojuegos. Se divide el mapa del nivel en cuadrados con un tamaño fijo. Cada uno de estos recuadros es conocido como TILE. En un determinado tile sólo puede haber un elemento. En nuestro caso hemos decidido crear un tamaño de tile que es casi estándar (32 x 32). En un tile habrá un elemento del nivel como puede ser el suelo o un cuadro decorativo, será la posición inicial de los personajes del juego y determinarán la posición inicial de los elementos del juego.

Esto supone que un nivel no sea más que un conjunto de tiles de 32 x 32 que definen el contenido de cada parte de dicho nivel. Como almacenar y cargar estos ficheros gestionando la información de cada uno de estos niveles lo veremos en la implementación de las clases del videojuego.

División en tiles
División en tiles

No hay mejor manera de entender lo que es un tile y como se crean los niveles a partir de ellos que utilizar el editor de niveles que viene con el videojuego final. Es buen momento de que practiques y crees tus primeros niveles. En la figura Tiles puedes ver un ejemplo de la construcción de un nivel a partir de tiles.

[editar] La lógica del juego

La lógica del juego sigue siendo la misma que estudiamos al principio del curso, no hay novedades importantes. Recuerda que se sigue un proceso de inicialización. En este caso la inicialización es más extensa. Hay que preparar todos los subsistemas que vamos a utilizar y determinar la posición de cada componente que verá dada por la información guardada en el fichero de niveles. En la figura tienes un recordatorio de las fases que teníamos que seguir a la hora de implementar un videojuego.

Una vez tengamos toda la información disponible mostraremos en pantalla nuestro juego. La lógica que sigue el juego, y que vamos a desarrollar a lo largo del capítulo, se basa en un bucle (el famoso game loop) que se va repitiendo a lo largo de la ejecución del programa hasta que el jugador decida salir de la aplicación. Recuerda que en este game loop se realiza una actualización lógica de las posiciones y estados de los personajes y demás elementos para luego para luego actualizar la pantalla con esta nueva información.

Lógica del juego
Lógica del juego

Durante la actualización lógica se comprueban las capturas de objetos, se gestionan las colisiones, se comprueba la temporización de la aplicación... Como puedes observar estos elementos no son nuevos para nosotros ya que los hemos desarrollado en el tutorial con el objetivo de integrarlos ahora en una única aplicación. Recuerda que después de esta actualización lógica se deben de mostrar los cambios a través del interfaz de pantalla.

En este capítulo detallaremos la implementación y lógica de cada uno de los elementos que componen el videojuego desde el editor hasta la función de salida.


[editar] El control del tiempo

Como ya sabes el control del tiempo es una de las partes fundamentales que tenemos que tener en cuenta a la hora de desarrollar nuestro videojuego. Este es uno de los aspectos que más se descuida cuando diseñamos nuestros primeros videojuegos ya que no es un problema hasta que no probamos el resultado del mismo.

Ya conoces los conceptos relacionados al control del tiempo de capítulos anteriores. Para nuestro videojuego de ejemplo utilizaremos una espera activa que fije unos determinados "frames per second" que sean suficiente para mostrar una fluidez correcta del videojuego.

Como en los demás apartados estudiaremos detalladamente la implementación de esta técnica en el apartado de la codificación de la aplicación.


[editar] La detección de colisiones

Como ya expusimos en el tema anterior la detección y gestión de colisiones es fundamental para crear un videojuego de calidad. Vamos a aplicar las técnicas estudiadas teniendo en cuenta la relación respuesta/rendimiento del sistema.

En este caso vamos a utilizar un algoritmo simple de colisiones que da un resultado bastante fiable a la hora de ejecutar el videojuego. No tienes más que probarlo. Se trata de ajustar el rectángulo que envuelve a nuestro personaje lo más posible para que no se produzcan efectos no desados. Sólo vamos a utilizar un rectángulo por participante ya que la carga de proceso que supone ampliar el número de áreas de colisiones no nos compensa con el resultado obtenido.

Según el tipo de colisión que se produzca deberemos de responder de manera diferente. Si nuestro personaje colisiona con un ítem deberá de eliminarlo del nivel y anotar dicho suceso. La colisión con estos ítems tiene que tener un propósito específico como puede ser el de aumentar nuestra puntuación, pasar de nivel al recogerlos todos o proporcionarnos una vida nueva.

En el caso de colisionar con enemigo deberemos de reflejar dos posibles casos. El primero sería que el personaje muriera. El número de vidas en principio lo contemplaremos como infinito para que puedas completar el videojuego dándole el comportamiento que más te guste. Basta con establecer un contador en el personaje que disminuya por colisión con el enemigo e incluir en el método al que llamamos cuando existe una colisión una llamada a salir al menú cuando sus vidas lleguen a cero.

El segundo caso a contemplar es que nuestro personaje esté en un determinado estado, golpeando por ejemplo, y procedamos a la eliminación del adversario en vez de la del protagonista. Dependiendo del comportamiento que vayamos a reflejar tendremos que avisar de esta colisión a una clase o a otra.

Todos los detalles sobre las desiciones de diseño e implementación las veremos en el diseño y codificación del juego.


[editar] ¿Por qué Programación Orientada a Objetos?

La realización de un programa es la respuesta a un problema. Para resolver un problema tenemos que descomponerlo en casos más sencillos y en tareas individuales para luego volver a construir, a unir, todas estas partes dándole solución al problema.

El criterio de decomposición del sistema ha sido el funcional identificando las funciones del sistemas y sus partes teniendo así partes más simples. Este método da buen resultado cuando dichas funciones están bien definidas y son estables en el tiempo. Si un sistema recibe cambios estructurales grandes esta descomposición en funciones se tambalea teniendo que, segurante, hacer cambios profundos en las aplicaciones que hayamos desarrollado.

La programación orientada a objetos integra las estructuras de datos y las funciones o métodos asociadas a ellas. Las funcionalidades se traducen en colaboraciones entre objetos que se realizan dinámicamente y las estructuras del programa no se ven amenazadas por un cambio en el mismo.

Uno de los principales motivos de utilizar programación orientada a objetos es la adecuación de este modelo a la programación de videojuegos. Podemos distinguir perfectamente los objetos que componen el videojuego y las funciones o capacidades asociadas a ellos los que nos permite que este enfoque sea perfecto para crear una abstracción mediante clases del problema.

Además utilizamos programación orientada a objetos por:


  • La orientación a objetos se aproxima más a la forma de pensar de las personas. Esto lo hace más comprensible y fácil de aplicar.
  • Proporciona abstracción, ya que en cada momento se consideran sólo los elementos que nos interesan descartando los demás.
  • Aumenta la consistencia interna al tratar los atributos y las operaciones como un todo.
  • Permite expresar características comunes sin repetir la información.
  • Facilita la reutilización de diseños y códigos.
  • Facilita la revisión y modificación de los sistemas desarrollados.
  • Origina sistemas más estables y robustos.
  • Se ha empleado con éxito para desarrollar aplicaciones tan diversas como compiladores, interfaces de usuario, sistemas de gestión de bases de datos, simuladores y, sobre todo, videojuegos.


[editar] Características de la Orientación a Objetos

Hoy en día el paradigma de la orientación a objetos ofrece una vista global de la Ingeniería del Software muy importante para realizar un desarrollo de calidad. Se trata de abordar los temas alrededor de los objetos, los participantes, de la aplicación y no entorno a las estructuras de datos.

Vamos a presentar brevemente las características de la orientación a objetos:


  • Abstracción: Las clases son la abstracción de un conjunto de objetos del mismo tipo.
  • Encapsulamiento: Permite proteger y englobar a los datos y la funcionalidad de dichos datos en una estructura que nos permite tenerlos asociados.
  • Ocultación de datos: Esta estructura permite tener varios tipos de datos. Entre ellos están los públicos y los privados ocultos al usuario.
  • Generalización: Una clase puede abstraerse en una clase más general y viceversa. Esto nos permite tener varios tipos de clases hijas de una clase madre según un discriminante con un comortamiento común o individual.
  • Polimorfismo: Nos permite funciones con el mismo nombre que se diferencien, por ejemplo, en el tipo de parámetros, que tengan comportamientos totalmente diferentes.
  • Clases y objetos: Un objeto es una istancia de una clase o lo que es lo mismo, es una clase inicializada. El objeto está próximo al mundo real mientras que la clase es una abstracción de éste.
  • Herramientas: Son cada una de las funcionalidades que nos aporta una clase para trabajar sobre tipos de datos como pueden ser sus atributos.
  • Atributos: Los atributos de un objeto son aquellas variables que definen dicho objeto. Las funciones de la clase deben de permitir trabajar con ellos para realizar las modificaciones pertinentes o simplemente para ser consultados ya que son la parte fundamental de dicho objeto.


Todas estas propiedades y características de la programación orientada a objetos la hacen ideal para la programación de videojuegos. Este tipo de programación sobre un modelo de proceso de desarrollo en espiral que nos permita realizar un desarrollo incremental y el patrón de diseño UML es todo lo que necesitamos para enfrentarnos a la construcción de un software de calidad.

[editar] Modelado

El modelado de la aplicación comprende el análisis y el diseño del software que debemos de realizar antes de escribir el código. Creamos un conjunto de modelos, como si fuesen planos a seguir del software, que nos permiten describir distintos aspectos como los requisitos que debe cumplir, la estructura a utilizar y el comportamiento que debe de tener dicho sistema.

Es fundamental el uso de modelos para integrar nuestra aplicación en un proceso de ingenería que nos permita crear un producto de calidad. Como podrás comprobar el modelado conlleva una preparación previa y un coste de tiempo considerable. Estos "inconvenientes" son compensados gracias a que el modelado proporciona un aumento de la productividad y calidad del software.

El modelado nos proporciona la documentación necesaria para desarrollar, implementar y mantener el proyecto así como un punto de partida para diseñar los planes de prueba del software.

La utilidad del modelado la podemos resumir en:

  • Mapa o visualización de cómo es o cómo queremos que sea el sistema.
  • Nos sirve para especficar el comportamiento y la estructura del sistema.
  • Es una guía que nos permite encauzar la construcción del sistema software.
  • Es una documentación válida acerca de las decisiones tomadas para la construcción del sofware.

La elección del modelado a utilizar es un aspecto crucial para el desarrollo software. El modelo a seguir tiene que ajustarse a nuestras necesidades reales. Un único modelo no es suficiente para analizar y diseñar un sistema. El desarrollo de un sistema se afronta mejor desde un conjunto de pequeños modelos casi independientes que nos proporcionen diferentes puntos de vista sobre el conjunto del sistema.

En nuestro caso la elección es simple. Vamos a realizar una aplicación en C++ orientada a objetos por lo que vamos a utilizar el modelado que mejor se ajusta a esta tipología, el UML.

El UML es un lenguaje de especificación para definir un sistema software que detalla la funcionalidad del sistema y documenta el proceso de desarrollo del mismo. UML cuenta con varios tipos de diagramas que veremos a continuación.

En el mundo del videojuego es importantísimo realizar un modelado adecuado. Muchos tipos de juegos son prácticamente sistemas de tiempo real que necesitan un gran nivel de refinamiento mientras que otros son demasiado complejos para abordar su implementación sin un "mapa" que nos guíe en el proceso de codificación. Aunque para neofitos en la materia puede parecer una pérdida de tiempo está demostrado que un buen planteamiento del problema, llevado a cabo por el modelado, es fundamental para la creación de un software de calidad así como para que el resultado de nuestro desarrollo cumpla unas condiciones de mantenibilidad y ampliación que sería una tarea casi imposible sin dicho mapa del software.

Vamos a hacer una introducción al proceso de desarrollo software necesario para la creación del videojuego especificando toda y cada una de las partes necesarias del proceso del mismo.


[editar] Especificación de los requisitos del sistema

En esta toma primera toma de contacto con el software que vamos a desarrollar tenemos que hacer una lista detallada y lo más completa posible de los requisitos que debe de cumplir el software. Para esto debemos de recabar la máxima información posible. Existen distintas técnicas para realizar esta tarea (como entrevistas, prototipado, ...). En este caso será un proceso intrínseco en el que dedicaremos un tiempo a planetarnos que queremos que haga nuestro videojuego. Vamos a seguir un esquema que nos servirá para especificar los requisitos de una forma más metódica.

[editar] Requisitos de interfaces externas

Tenemos que describir de forma detallada los requisitos de conexión a otros sistemas hardware o software con los que vamos a interactuar así como los prototipos de las ventanas e interfaz de usuario así como de los informes que se hayan de generar.

El interfaz entre la aplicación y el hardware lo proporciona la librería SDL. Mediante esta librería vamos a acceder a las características necesarias que tenemos que modificar en los distintos dispositivos. Es un aspecto que no tendremos que analizar y diseñar ya que está preestablecido y sólo haremos uso de ello. SDL proporciona el interfaz con el teclado, el ratón, el joystick, el hardware gráfico y los dispositivos de sonido. Para interactuar con el sistema operativo utilizaremos funciones propias del lenguaje C++ ya que las tareas que realizaremos son triviales como reservar memoria o utilizar ficheros de almancenamiento.

Vamos a definir el interfaz entre el videojuego y el usuario. Todas las ventanas de la aplicación podrán ser mostradas a pantalla completa o en formato de ventana con una resolución de 640 x 480 píxeles. Existen tres tipos de ventanas con las que el usuario puede interactuar con la aplicación que pasamos a definir:

  • Ventana de Menú: Esta ventana mostrará el título de la aplicación y un menú que nos permita elegir entre editar los niveles, jugar o salir de la aplicación. Este menú debe de tener un ítem o efecto que nos permita conocer sobre que opción estamos situados. El usuario interactuará con la aplicación mediante las flechas cursoras arriba y abajo para navegar por las opciones y la tecla enter para seleccionar una de ellas. El prototipo de la ventana es el de la figura.
Prototipo: Ventana Menú
Prototipo: Ventana Menú
  • Editor de Niveles: Esta ventana nos debe permitir modificar o crear cómodamente los niveles del a aplicación. Debe de tener un botón que nos permita limpiar el contenido del nivel así como guardar las modificaciones del mismo o descartarlas. Debe de tener un menú que nos permita seleccionar los elementos que queremos añadir al nivel que tenemos en el área de edición. El dispositivo de entrada para esta ventana será el ratón ya que se adapta perfectamente a las necesidades de esta parte de la aplicación. Debe de existir de indicador que nos permita conocer la posición del cursor en un momento dado así como un elemento que nos resalte qué nivel estamos editando. Para salir del editor dispondremos de un icono así como la tecla ESC para volver al menú principal. El prototipo de la ventana de edición es el de la figura.
Prototipo: Editor de Niveles
Prototipo: Editor de Niveles
  • Ventana de Juego: Proporciona el interfaz para interactuar con los niveles diseñados en la aplicación. Es una ventana sin ningún tipo de elemento adicional que nos permite movernos por toda la superficie del mapa del nivel con las restricciones impuestas por el movimiento del personaje principal. La forma de interactuar con esta parte de la interfaz son las teclas cursoras que nos permitirán mover al protagonista por el nivel y la tecla ESC para volver al menú principal. No procede prototipo de esta ventana ya que no hay ningún requisito específico que mostrar.

[editar] Requisitos funcionales

En este apartado debemos de responder a la pregunta sobre qué debe de hacer la aplicación o sistema.

Nuestra aplicación tiene dos requisitos principales:

  • Gestionar los niveles de la aplicación y permitir que el usuario realice las modificaciones que cree pertinentes.
  • Permitir que el usuario interactúe con los niveles y juegue con la aplicación.
  • Debe de permitir salir de la aplicación en cualquier momento.

Se trata de definir las trazas a grandes rasgos de lo que debe de hacer el sistema. El cómo debe de hacerlo lo indicaremos en otra etapa del desarrollo.

[editar] Requisitos de rendimiento

En este apartado tenemos que indicar los requisitos relativos al rendimiento de la aplicación tal como tiempos de respuesta y otros aspectos.

Nuestra aplicación podemos considerarla de tiempo real blando ya que establecemos unos periodos que deben de cumplirse para una correcta funcionalidad de la aplicación pero no el margen de error es flexible. Nos marcamos el objetivo de poder mostrar cien frames por segundo. La aplicación deberá estar optimizada sobre el parámetro del tiempo sacrificando el consumo de memoria principal.

[editar] Restricciones de diseño

Ahora vamos a especificar aquellas restricciones que se hayan identificado e influyan en el diseño del sistema.

El diseño de la aplicación tiene que primar los tiempos de respuesta sobre el consumo de recursos de espacio como la memoria principal o secundaria. Esta es la principal restricción que tendrá el diseño de nuestro videojuego. Las demás características tendrán que ser diseñadas basándose en este principipo.

El grado de cohesión y acoplamiento de las clases puede verse afectado por esta restricción. Es necesario evaluar estos parámetros con el objetivo anteriormente expuesto.

[editar] Atributos del sistema software

En este apartado debemos de especificar todos los atributos con los que debe de cumplir nuestra aplicación.

Uno de los requisitos principales de la aplicación es que sea portable entre los sistemas compatibles con SDL. No podremos utilizar código dependiente del sistema operativo donde desarrollemos nuestra aplicación. Debe de ser un código mantenible y ampliable que podamos mejorar en futuras versiones del mismo perfeccionando todos los aspectos que sean necesarios o que queramos modificar.

La aplicación debe ser robusta y fiable desde su diseño. La seguridad no es un tema relevante ya que no vamos a requerir ningún tipo de dato que sea conflictivo o susceptible de ser protegio.

[editar] Otros requisitos

En este apartado especificaremos todos los resquisitos, que por su naturaleza, no hayan podido ser incluidos en otros apartados del tutorial.

Para este videojuego la aplicación, su diseño y codificación deberán de tener una fuerte componente didáctica que integre el temario visto en los distintos capítulos del tutorial con el fin de que el lector de dicho temario pueda tomar las implementaciones expuestas en este ejemplo como guía para elaborar su propio videojuego.


[editar] Análisis

En el análisis del sistema se realizan los modelos que nos ayudan a naalizar y especificar el comportamiento del sistema. Existen varios tipos de modelado nostros vamos a utilizar un efoque orientado a objetos usando la notación UML.

En este apartado vamos a realizar un análisis del problema a resolver con el objetivo de describir qué debe de hacer el sistema software y no, todavía, cómo lo hace. Es un paso más entre el problema y su resolución final. Es fundamental a la hora de desarrollar un videojuego hacer un análisis detallado de lo que queremos que haga nuestro videojuego. Es importante que creemos un sistema potente pero que, si es uno de nuestros primeros proyectos, no se nos escape de las manos.

El análisis será el primer paso que nos guiará en la construcción de nuestro videojuego. Para este paso vamos a utilizar el modelado UML. UML es el lenguaje de modelado más utilizado en la actualidad. Es un lenguaje gráfico para visualizar, especificar, construir y documentar sistemas software. Nos permite especificar el sistema, que no describir métodos o procesos, y se define sobre una serie de diagramas que estudiaremos a continuación.

[editar] Modelo de Casos de Uso

El modelo de casos de uso de UML especifica que comportamiento debe de tener el sistema software. Representa los requisitos funcionales del sistema centrándose en qué hace y no en cómo lo hace. Este modelo no es orientado a objetos por lo que podemos utilizarlo en proyectos que no lo sean.

[editar] Casos de uso

El caso de uso está compuesto de:


  • Conjunto de secuencia de acciones Cada una de estas secuencias representa un posible comportamiento del sistema.
  • Actores Roles o funciones que pueden adquirir cada usuario, dispositivo u otro sistema al interaccionar con nuestro sistema. El tempo puede ser considerado un sistema por lo que puede ser un actor. Los actores no son parte del sistema en sí. Hay dos tipos de actores
    • Principales Demanda al sistema el cumplimiento de un objetivo.
    • Secundarios Se necesita de ellos para cumplir con un objetivo.
  • Variantes Casos especiales de comportamiento.
  • Escenarios Es una secuencia de interacciones entre actores y el sistema. Está compuesto de un flujo principal y flujos alternativos o excepcionales. Es una instancia de un caso de uso.


Los casos de uso son iniciados por un actor con un objetivo concreto y es completado con éxito cuando el sistema cumple con dicho objetivo. Existen secuencias alternativas que nos pueden llevar al éxito o al fracaso en el cumplimiento del objetivo. El conjunto completo de los casos de uso especifica todas las posibles formas de usar el sistema que no es más que el comportamiento requerido de dicho sistema.

Los casos de uso ofrecen un medio eficiente para capturar requisitos funcionales centrándose en las necesidades del usuario. Dirigen el proceso al desarrollo a que las actividades que conlleva este desarrollo se realizan a partir de los casos de uso.

Existen tres tipos de relaciones en los casos de uso:

  • Generalización Un caso de uso hereda el comportamiento y significado de otro.
  • Inclusión Un caso de uso incorpora explícitamente el comportamiento de otro en algún lugar de su secuencia.
  • Extensión Un caso de uso amplía la funcionalidad de otro incluyendo implícitamente el comportamiento de otro caso de uso.


[editar] Diagramas de casos de uso

Una vez estudiada esta pequeña introducción a los casos de uso vamos a realizar el diagrama que representa la funcionalidad de nuestro videojuego de ejemplo. Es importante que dediques un tiempo importante a este proceso antes de empezar a implementar el videojuego porque entre la documentación generada y la vista general del proyecto que te proporciona este diagrama te ahorrarán muchos quebraderos de cabeza.

Para obtener los casos de uso seguiremos el siguiente procedimiento:

  1. Identificaremos a los usuarios del sistema y los roles que juegan en dicho sistema.
  2. Para cada role identificaremos todas las maneras de interactuar con el sistema.
  3. Creamos un caso de uso para cada objetivo que queramos cumplir.
  4. Estructuraremos los casos de uso.

El diagrama de casos de uso es una ayuda visual pero lo realmente importante se encuentra en la descripción de los casos de uso. Vamos a seguir el esquema anterior para crear nuestro diagrama de casos de uso.

El usuario de nuestro sistema es único. El mismo será el que edite los niveles así como juegue a sus creaciones. No hay diferenciación de roles ya que siempre realizará la función de usuario del sistema software.

Ahora vamos a identificar las maneras que tiene el usuario de interactuar con el sistema. Son tres. La primera se produce cuando el jugador decide jugar a los niveles creados. La segunda es cuando el usuario decide editar los niveles del videojuego para modificar algún detalle del mismo o crear niveles nuevos. La tercera está relacionada con la petición de salir de la propia aplicación. Para cada uno de estos objetivos crearemos un caso de uso que mostraremos en el diagrama de la figura.

Diagrama de casos de uso: Videojuego de ejemplo
Diagrama de casos de uso: Videojuego de ejemplo


[editar] Descripción de los casos de uso

La descripción de un caso de uso es un texto que puede ser expresado de varias formas. Nosotros vamos a utilizar una notación formal usando plantillas. Este texto debe ser legible y comprensible por un usuario que no sea experto. Para describir los casos de uso vamos a utilizar una plantilla en formato completo que nos permita dejar fuera de toda duda razonable los casos de uso requeridos en nuestro videojuego.

Vamos a proceder a describir los casos de uso de nuestro videojuego:

DESCRIPCIÓN CASO DE USO: Cargar Niveles

  • Caso de uso: Cargar Niveles
  • Descripción: Carga el fichero de niveles de juego en la aplicación desde un fichero de datos.
  • Actores: Usuario
  • Precondiciones: Para realizar dicha acción deben de existir niveles guardados en el fichero de niveles.
  • Postcondiciones: Los datos del primer nivel almacenado serán mostrados en pantalla para poder interaccionar con dicho nivel.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda la carga de los niveles.
  2. El sistema carga los niveles.
  3. El sistema comprueba que el fichero existe.
  4. El sistema comprueba que al menos el primer nivel existe.
  5. El sistema carga el nivel en la aplicación.
  6. El sistema muestra el primer nivel al usuario.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 1a Carga de nivel automática por secuencialidad del juego.
    • 1b Carga de nivel a demanda del usuario mediante un interfaz.
    • 3a El fichero no existe en el sistema.
      • El sistema muestra el error y cierra el sistema.
    • 4a El nivel no existe en el fichero.
      • El sistema muestra el error y cierra el sistema.


DESCRIPCIÓN CASO DE USO: Siguiente Nivel


  • Caso de uso: Siguiente Nivel
  • Descripción: Muestra el nivel siguiente al cargado actualmente.
  • Actores: Usuario
  • Precondiciones: Deben de existir niveles cargados en el sistema.
  • Postcondiciones: El siguiente nivel será mostrado en pantalla.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda la carga del siguiente nivel.
  2. El sistema comprueba que existe un siguiente nivel.
  3. El sistema muestra el nivel al usuario.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 1a Carga de nivel automática por secuencialidad del juego.
    • 1b Carga de nivel a demanda del usuario mediante un interfaz.
    • 2a El nivel no existe en el sistema.
      • El sistema muestra el error y no se avanza de nivel.
  • No funcional: El interfaz del usuario proporcinará dos botones de navegación por los niveles del fichero fácilmente identificables.


DESCRIPCIÓN CASO DE USO: Nivel Anterior

  • Caso de uso: Nivel Anterior
  • Descripción: Muestra el nivel anterior al cargado actualmente.
  • Actores: Usuario
  • Precondiciones: Deben de existir niveles cargados en el sistema.
  • Postcondiciones: El nivel anterior será mostrado en pantalla.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda la carga del nivel anterior.
  2. El sistema comprueba que existe un nivel anterior.
  3. El sistema muestra el nivel al usuario.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 1a El nivel no existe en el sistema.
      • El sistema muestra el error y no se retrasa de nivel.


DESCRIPCIÓN CASO DE USO: Nuevo Nivel

  • Caso de uso: Nuevo Nivel
  • Descripción: Crea un nivel nivel para ser editado.
  • Actores: Usuario
  • Precondiciones: Debe estar cargado el fichero de niveles.
  • Postcondiciones: Muestra un nivel vacío a editar por el usuario.
  • Escenario principal:Describimos el escenario principal:
  1. El usuario demanda un nuevo nivel.
  2. El sistema crea dicho nivel.
  3. El sistema muestra el nivel al usuario.
  • Extensiones (Flujo alternativo):Describimos el flujo alternativo:
    • 2a El nivel no se puede crear en el sistema.
      • El sistema muestra el error y no se continúa.


DESCRIPCIÓN CASO DE USO: Editar Nivel

  • Caso de uso: Editar Nivel
  • Descripción: Editamos un nivel de los disponibles en el sistema.
  • Actores: Usuario
  • Precondiciones: El nivel a editar debe estar cargado en el sistema.
  • Postcondiciones: Modificamos un determinado nivel.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda editar un nivel.
  2. El sistema prepara dicho nivel.
  3. El usuario edita el nivel.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 2a El nivel no está disponible en el sistema.
      • El sistema muestra el error y se reinicia la aplicación.
  • Cuestiones pendientes: Se debe guardar la modificación del nivel para que esté disponible en otros escenarios.


DESCRIPCIÓN CASO DE USO: Guardar Nivel

  • Caso de uso: Guardar Nivel
  • Descripción: Guarda el nivel que muestra el sistema en el fichero de niveles.
  • Actores: Usuario
  • Precondiciones: Debe de existir un nivel cargado que guardar.
  • Postcondiciones: Guarda el nivel en el fichero de niveles.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda guardar un nivel.
  2. El sistema guarda el nivel.
  3. El sistema muestra el resultado de la operación.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 3a El nivel no puede ser guardado.
    • El sistema muestra el error.
    • 3b El nivel es guardado.
  1. El sistema muestra OK.


DESCRIPCIÓN CASO DE USO: Jugar

  • Caso de uso: Jugar
  • Descripción: Nos permite interactuar con los niveles creados en el sistema.
  • Actores: Usuario
  • Precondiciones: Deben existir niveles en el sistema.
  • Postcondiciones: Se muestran los niveles y se nos permite interactuar con ellos secuencialmente.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda interactuar con el sistema.
  2. El sistema carga un nivel.
  3. El usuario interacúa con el sistema.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 2a El nivel no puede ser cargado.
  1. El sistema muestra el error y reinicia la aplicación.
    • 3a El usuario completa un nivel.
  1. Se vuelve al paso 3 y se carga otro nivel.
    • *a El usuario decide teminar.
  1. Se cierra la aplicación.
  • No funcional: Se dispondrá de un dispositivo de juegos para que el usuario pueda interactuar con el sistema.


DESCRIPCIÓN CASO DE USO: Gestionar Niveles

  • Caso de uso: Gestionar Niveles
  • Descripción: Gestiona el fichero de niveles del sistema.
  • Actores: Usuario
  • Precondiciones:
  • Postcondiciones: Realiza una operación sobre el fichero de niveles.
  • Escenario principal: Describimos el escenario principal:
  1. El usuario demanda una operación con los niveles.
  2. El sistema realiza la operación.
  3. El sistema muestra el resultado de la operación.
  • Extensiones (Flujo alternativo): Describimos el flujo alternativo:
    • 2a La operación demandada es crear un nivel.
  1. Incluir (Nuevo Nivel)
    • 2b La operación demandada es cargar un nivel.
  1. Incluir (Cargar Nivel)
    • 2c La operación demandada es pasar de nivel.
  1. Incluir (Siguiente Nivel)
    • 2d La operación demandada es volver a un nivel anterior.
  1. Incluir (Nivel Anterior)
    • 2e La operación demandada es guardar un nivel.
  1. Incluir (Guardar Nivel)
  • 2f La operación demandada es editar un nivel.
  1. Incluir (Editar Nivel)


[editar] Modelo conceptual de datos en UML

El siguiente paso del análisis de la aplicación que estamos realizando es realizar el modelo conceptual de datos. Este tipo de modelado sirve para especficiar los requisitos del sistema y las relaciones estáticas que existen entre ellos.

Para realizar este modelo se utiliza como herramienta los diagramas de clase. En estos diagramas representamos las clases de objetos, las asociaciones entre dichas clases, los atributos que componen las clases y las relaciones de integridad.

[editar] Conceptos básicos

Vamos a presentar varios conceptos, que aunque ya los hemos utilizado en el tutorial, no está demás recordarlos. El primero de ellos es el concepto de objeto. Un objeto no es más que una entidad que existe en el mundo real bien distinguible de las demás entidades. Un ejemplo de objeto es un bolígrafo, una persona, una factura...

Los objetos de un mismo tipo se abstraen en clases. Estas clases describen a un conjunto de objetos con las mismas propiedades, comportamientos comunes, con relaciones idénticas con los otros objetos y una semántica común.

Existen varios tipos de relaciones entre las distintas clases. Estas relaciones normalmente tienen asociadas unas restricciones de participación en dichas relaciones. Algunos ejemplos de estos tipos de relaciones son las agregaciones, las composiciones, las generalizaciones... El tema de las relaciones entre clases es un tema muy extenso e interesante en el que es necesario que profundices para poder realizar un diseño de calidad.

[editar] Descripción diagramas de clases conceptuales

En este punto debemos de tener claro que clases vamos a necesitar para implementar nuestro videojuego. Es el momento de presentarlas describiendo brevemente las características de cada una de ellas para, posteriormente, realizar el diagrama de clases.


  • Imagen: Esta clase nos permite controlar todos los aspectos referentes a las imágenes necesarias en la aplicación.
  • Música: Esta clase nos permite controlar la música del videojuego.
  • Sonido: La clase sonido nos permite gestionar los sonidos del videojuego.
  • Fuente: Nos permite utilizar una fuente para rotular en la aplicación.
  • Texto: Con esta clase creamos y controlamos los textos necesarios en la aplicación.
  • Galería: La galería englobará todos los contenidos multimedia de la aplicación. Será la encarga de gestionar todos los aspectos de este contenido, desde la inicialización de los elementos, hasta la utilización de éstos por parte de las demás clases de la aplicación.
  • Participante: Esta clase será el interfaz de todos los participantes del juego. Nos permitirá realizar las actualizaciones lógicas y gráficas de todos los elementos existentes en un nivel.
  • Protagonista: Subclase de Participante que nos permitirá controlar los aspectos del personaje principal del juego.
  • Item: Clase heredada de Participante que nos permite controlar los objetos e ítems del nivel y su comportamiento.
  • Enemigo: Clase hija de Participante que controla todos los aspectos de los enemigos.
  • Control Movimiento: Clase auxiliar que nos permite establecer un tipo de movimiento a diferentes clases del sistema.
  • Control Animación: Controla las animaciones internas de todos los elementos de la aplicación a partir de una rejilla de imágenes.
  • Control Juego: Lleva el control de la lógica y los elemetos del juego. Gestiona las colsiones así como los elementos existentes en un determinado nivel, sus actualizaciones lógicas y gráficas.
  • Menu: Esta clase controla el menú principal de la aplicación y sus distintas opciones.
  • Juego: Esta clase controla los aspectos relevantes de la aplicación en tiempo de juego.
  • Editor: Con esta clase controlaremos los aspectos relevantes de la edición y gestión de niveles.
  • Interfaz: Esta clase hace de interfaz entre las diferentes escenas de la aplicación para la navegación de ellas que no son otras que Menu, Juego y Editor.
  • Nivel: Controla las propiedades y proporciona las capacidades para trabajar con niveles.
  • Ventana: Esta clase nos permitirá mostrar sólo una porción adecuada de la superficie del nivel
  • Universo: Es la clase principal que interrelaciona todos los aspectos del juego.
  • Teclado: Esta clase nos permite gestionar la entrada de teclado.
  • Apuntador: Con la clase Apuntador podremos utilizar el dispositivo de ratón en el editor de niveles.


[editar] Diagrama de clases

Una vez descritas todas las clases y las relaciones entre ellas vamos a presentar los diagramas de clases que nos van a permitir tener un mapa conceptual de la globalidad de la aplicación para afrontar su desarrollo. Vamos a presentar los diagramas por subconjuntos. Así podremos estudiar los detalles de cada uno de ellos. Para terminar incluiremos un diagrama donde observar las relaciones entre todas las clases que intervendrán en la aplicación.

Para descomponer el diagrama hemos tomado el criterio de funcionalidad. Mostraremos subconjuntos de clases que tengan un fin común.

El primer subdiagrama que presentamos incluye a las clases que gestionarán los elementos multimedia de la aplicación. Podemos decir que la clase principal de este grupo es la clase Galería que estará compuesta de las clases que están asociadas a ella. El diagrama es el de la figura.

Diagrama de clases: Componentes multimedia
Diagrama de clases: Componentes multimedia


En este diagrama se muestran siete clases. Como hemos comentado Galería es la clase que gestiona a las demás.

El segundo diagrama que presentamos relaciona a las clases que nos permiten crear y modificar niveles en la aplicación. En este caso la clase que permite relacionar a las demás componentes del diagrama es la clase Universo aunque la que contendrá toda la lógica para realizar esta tarea es la clase Editor.

Diagrama de clases: Editando niveles
Diagrama de clases: Editando niveles

El tercer diagrama que vamos a estudiar engloba las clases que están asociadas directamente con la clase Participante que recoge todos los elementos activos que participan en los niveles del juego.

Diagrama de clases: Participantes
Diagrama de clases: Participantes

Ahora vamos a estudiar uno de los diagramas de clases más importantes del análisis. Se trata de la clase Universo encargada de relacionar las clases del videojuego, los elementos de control y los elementos multimedia que representa a cada una de estas partes.

Diagrama de clases: Universo
Diagrama de clases: Universo


Para terminar con este apartado vamos a presentar el diagrama de clases que relaciona todos los demás diagramas. Al ser excesivamenete grande no vamos a detallar cada uno de sus componentes ya que esta tarea la realizamos en los anteriores diagramas.

Diagrama de clases: Diagrama Global
Diagrama de clases: Diagrama Global


[editar] Modelo de comportamiento del sistema

El modelo del comportamiento especifica cómo debe de actuar un sistema. El sistema a considerar es el que engloba a todos los objetos. Este modelo consta de dos partes. La primera es el diagrama de secuencia de sistema que nos muestra la secuencia de eventos entre los actores y el sistema.La segunda parte de este modelo está compuesta por los contratos de las operaciones del sistema que efecto que deben producir las operaciones del sistema.

[editar] Diagramas de secuencia del sistema y los Contratos de operaciones

Una vez descrito un caso de uso representamos mediante un diagrama de secuencia del sistema dicho caso de uso. Este diagrama nos servirá de aproximación visual a los casos de uso como complemento a la descripción anterior.

La principal utilidad y objetivo de este diagrama es permitirnos identificar las operaciones y eventos del sistema. El punto de partida de estos diagramas son los casos de uso que nos permiten identificar qué eventos son los que van de los actores hacia el sistema. Tendremos que definir un diagrama de secuencia para cada escenario relevante de un caso de uso. Para identificar estos escenarios en el sistema nos centraremos en los eventos que genera el usuario que hace uso de dicho sistema y que espera alguna operación como respuesta de dicha interacción.

La segunda parte del modelo es la realización de los contratos para las operaciones del sistema. Estos contratos describen el efecto que deben producir las operaciones del sistema. Las operaciones del sistema osn operaciones internas que se ejecutan como respuestas a un evento producido, normalmente, por un actor o usuario.

Los contratos tienen unas partes bien diferenciadas: nombre de la operación, responsabilidades, precondiciones, postcondiciones... que utilizaremos en forma de plantilla. Constriuremos contratos para operaciones complejas y para aquellas que no quedan claras en el proceso de especificación del sistema.


[editar] Diagramas de secuencia del sistema y Contratos de las operaciones del sistema

A continuación expondremos todos los diagramas de secuencia del sistema y los correspondientes contratos de las operaciones del sistema.

DIAGRAMA DE SECUENCIA: Cargar Niveles

Diagrama de secuencia: Cargar Niveles
Diagrama de secuencia: Cargar Niveles


Contrato de las operaciones

  • Operación: Cargar Niveles
  • Responsabilidades: Carga los niveles del juego en memoria principal. Primero comprueba que existe el fichero de niveles y seguidamente que dicho fichero tenga algún nivel. De ser así carga el primero de ellos. Si no existe el fichero o está vacío se muestra el error y se cierra la aplicación.
  • Precondiciones: Debe de existir un fichero de niveles. Se debe de solicitar la carga de los niveles explícita o implícitamente asociado a alguna acción del usuario.
  • Postcondiciones:
    • Carga en memoria principal el primer nivel de la colección almacenada en el fichero de niveles.
    • Si no existe el fichero de niveles muestra un mensaje de error y termina la aplicación.
  • Operación: Muestra Nivel
  • Responsabilidades: Mostrar en pantalla el nivel actual de la aplicación. Si no exisistiese dicho nivel se muestra el error y se cierra la aplicación.
  • Precondiciones: El nivel a mostrar debe estar cargado en memoria principal.
  • Postcondiciones:
    • Carga en memoria principal el primer nivel de la colección almacenada en el fichero de niveles.
    • Si no hay nivel que mostrar se muestra un mensaje de error y se sale de la aplicación.


DIAGRAMA DE SECUENCIA: Siguiente Nivel

Diagrama de secuencia: Siguiente Nivel
Diagrama de secuencia: Siguiente Nivel


Contrato de las operaciones

  • Operación: Carga Nivel Siguiente
  • Responsabilidades: Cargar el siguiente nivel al actual. En caso de no existir el siguiente nivel no realiza acción alguna.
  • Precondiciones: Debe de existir un nivel cargado.
  • Postcondiciones:
    • Prepara el siguiente nivel disponible para ser mostrado.
    • En caso no existir no realiza ninguna acción.
  • Operación: Muestra Nivel
  • Responsabilidades: Mostrar en pantalla el nivel actual de la aplicación. Si no exisistiese dicho nivel se muestra el error y se cierra la aplicación.
  • Precondiciones: El nivel a mostrar debe estar cargado en memoria principal.
  • Postcondiciones:
    • Carga en memoria principal el primer nivel de la colección almacenada en el fichero de niveles.
    • Si no hay nivel que mostrar se muestra un mensaje de error y se sale de la aplicación.


DIAGRAMA DE SECUENCIA: Nivel Anterior

Diagrama de secuencia: Nivel Anterior
Diagrama de secuencia: Nivel Anterior

Contrato de las operaciones

  • Operación: Carga Nivel Anterior
  • Responsabilidades: Cargar el siguiente anterior al actual. En caso de no existir el siguiente nivel no realiza acción alguna.
  • Precondiciones: Debe de existir un nivel cargado.
  • Postcondiciones:
    • Prepara el nivel anterior disponible para ser mostrado.
    • En caso no existir no realiza ninguna acción.
  • Operación: Muestra Nivel
  • Responsabilidades: Mostrar en pantalla el nivel actual de la aplicación. Si no exisistiese dicho nivel se muestra el error y se cierra la aplicación.
  • Precondiciones: El nivel a mostrar debe estar cargado en memoria principal.
  • Postcondiciones:
    • Carga en memoria principal el primer nivel de la colección almacenada en el fichero de niveles.
    • Si no hay nivel que mostrar se muestra un mensaje de error y se sale de la aplicación.


DIAGRAMA DE SECUENCIA: Nuevo Nivel

Diagrama de secuencia: Nuevo Nivel
Diagrama de secuencia: Nuevo Nivel


Contrato de las operaciones

  • Operación: Crea un Nivel Nuevo
  • Responsabilidades: Crear un nivel nuevo al final del a lista de niveles. En caso de no poder alojar el nivel mostrar un mensaje de error.
  • Precondiciones: Debe de existir el fichero de niveles.
  • Postcondiciones:
    • Prepara un nivel vacío para ser mostrado y editado.
    • Si no se puede preparar dicho nivel se muestra un mensaje de error.
  • Operación: Muestra Nivel
  • Responsabilidades: Mostrar en pantalla el nivel actual de la aplicación. Si no exisistiese dicho nivel se muestra el error y se cierra la aplicación.
  • Precondiciones: El nivel a mostrar debe estar cargado en memoria principal.
  • Postcondiciones:
    • Carga en memoria principal el primer nivel de la colección almacenada en el fichero de niveles.
    • Si no hay nivel que mostrar se muestra un mensaje de error y se sale de la aplicación.


DIAGRAMA DE SECUENCIA: Editar Nivel

Diagrama de secuencia: Editar Nivel
Diagrama de secuencia: Editar Nivel


Contrato de las operaciones

  • Operación: Editar Nivel
  • Responsabilidades: Prepara un nivel para ser editado y muestra el interfaz.
  • Precondiciones:
  • Postcondiciones: En caso de no existir el fichero de niveles se crea.


DIAGRAMA DE SECUENCIA: Guardar Nivel


Diagrama de secuencia: Guardar Nivel
Diagrama de secuencia: Guardar Nivel


Contrato de las operaciones

  • Operación: Guarda el Nivel
  • Responsabilidades: Guardar el nivel actual en el fichero de niveles. En caso de no poder guardarlo muestra el error.
  • Precondiciones: Debe de existir un nivel cargado en memoria que guardar.
  • Postcondiciones:
    • Guarda el nivel en el fichero de niveles.
    • En caso de no poder realizar la acción el sistema muestra un mensaje.


DIAGRAMA DE SECUENCIA: Jugar

Diagrama de secuencia: Jugar
Diagrama de secuencia: Jugar


Contrato de las operaciones

  • Operación: Jugar
  • Responsabilidades: Prepara el sistema, los niveles y la lógica del juego para una partida. En caso de no poder realizar alguna de las operaciones muestra un mensaje de error y termina la aplicación.
  • Precondiciones: Deben de existir niveles en el fichero de niveles. Dicho fichero debe existir también. Se debe demandar la acción de jugar.
  • Postcondiciones:
    • Devuelve el sistema preparado para jugar.
    • En caso de error muestra un mensaje y cierra la aplicación.
  • Operación: Juega
  • Responsabilidades: Devolver el control al usuario para que pueda interactuar con la aplicación.
  • Precondiciones: El sistema debe de haber sido preparado para que el usuario interactúe con la aplicación.
  • Postcondiciones: Pasa el control de la aplicación al usuario.


DIAGRAMA DE SECUENCIA: Gestionar Niveles


Diagrama de secuencia: Gestionar Niveles
Diagrama de secuencia: Gestionar Niveles


Contrato de las operaciones

  • Operación: Solicitar Nivel
  • Responsabilidades: Solicitar al sistema una determinada acción sobre un nivel.
  • Precondiciones: El nivel solicitado debe de existir.
  • Postcondiciones: El sistema recibe la petición del usuario.
  • Operación: Resultado de la acción.
  • Responsabilidades: Debe de devolver al usuario el resultado de la acción previamente solicitada.
  • Precondiciones: Debe de existir una acción solicitada al sistema sobre un nivel.
  • Postcondiciones: Dependiendo del resultado de la acción
    • Devuelve al usuario un mensaje con el resultado de la acción.
    • Devuelve un nivel determinado a otro proceso de usuario.


[editar] Diseño del Sistema

Siguiendo con la metodología del análisis vamos a realizar un diseño orientado a objetos mediante UML. Hasta ahora habíamos especificado y analizado nuestra aplicación con lo que completamos el análisis de requisitos y la especificación del sistema. Ahora vamos a diseñar el sistema que no es más que describir fuera de toda duda razonable que va a hacer el sistema.

El diseño del sistema es la actividad de aplicar diferentes técnicas y principios con el propósito de definir un sistema con el suficiente detalle para que se pueda implementar. El resultado del diseño es un conjunto de diseños de diferentes partes del software.

Este proceso es mucho más sencillo una vez que hemos especificado qué debe hacer el sistema en los pasos anteriores. El proceso de diseño consiste en especificar el sistema con suficiente detalle para que se pueda implementar. El resultado del proceso de diseño es una arquitectura de software bien definida y los diseños de datos, interfaz y programas que nos permitan llevar a cabo la implementación del sistema software con garantías de éxito.

Para llevar a cabo el diseño vamos a usar patrones de diseño que nos permiten plantear los problemas concretos donde definiremos el contexto del problema, el problema en sí y la solución para dicho problema.

[editar] Arquitectura del sistema software

La arquitectura del sistema software un esquema de organización estructural para estos sistemas. Con él especificaremos las responsabilidades de cada uno de los subsistemas definidos para organizar las relaciones entre ellos. Utilizaremos el patrón de arquitectura en capas.

El contexto del problema es que necesitamos descomponer nuestra aplicación en grupos de tareas con un mismo nivel de abstracción que sirvan de base a otras tareas más complejas.

El problema existente es que necesitamos que esta jerarquía esté bien definida para desarrollar nuestro software ya que las tareas de alto nivel no se pueden implementar utilizando los servicios de la plataforma ya que aumentaría la dependencia de nuestro sistema por lo que necesitamos servicios intermedios. Hay varias características que son deseables para nuestras aplicaciones. Deben ser:


  • Mantenibles: Un cambio en el código no puede propagarse a todo el sistema.
  • Reusable: Los componentes de nuestra aplicación deben de poder ser utilizados en otras aplicaciones por lo que deberemos de separar interfaz e implementación.
  • Cohesión: Responsabilidades similares deben agruparse para favoreces la mantenebilidad y la compresión del sistema.
  • Portabilidad: Esta es una característica deseable que no siempre puede satisfacerse.


En nuestro caso utilizamos SDL que nos proporciona un nivel más en la jerarquía de niveles con la que conseguimos la independencia del sistema software del sistema operativo y de cualquier hardware en el que queramos compilar nuestra aplicación.

La solución es estructurar al sistema en un número de capas suficiente para que cumpla todas las características deseables para nuestra aplicación. Los servicios que ofrece la capa n deben basarse en los servicios que ofrece la capa n-1 y la propia n. Los componentes de una capa han de estar al mismo nivel de abstracción.

Para nuestra aplicación SDL vamos a usar una arquitectura en tres capas. La primera de ellas será la capa de presentación que será la encargada de la interacción (interfaz) con el usuario. La segunda capa será la capa de dominio que es la responsable de la implementación de la funcionalidad del sistema. La tercera y última capa será la encargada de interactuar con el sistema para almacenar y obtener la información estática (o almacenada en disco) del sistema a la que llamaremos capa de gestión de datos.

Una vez determinado el patrón arquitectónco, el patrón de diseño nos proporciona una esquema que nos permite refinar los componentes y las relaciones de los subsistemas. Resuelven un problema de diseño general en un contexto determinado. Vamos a utilizar el patrón de diseño de UML.

No necesitamos realizar un diseño de la capa de gestión de datos ya que los únicos datos que vamos a almacenar en el disco es un fichero organizado secuencialmente donde estarán los distintos niveles de la aplicación. De la gestión del acceso a disco se encargará la clase que administrará los niveles consiguiendo una cohesión importante de la clase.

[editar] Diseño de la capa de dominio

En los siguientes apartados vamos a realizar el diseño de la capa de dominio, o lo que es lo mismo, la capa encargada de la implementación de la funcionalidad del sistema.

Una vez realizado los diagramas de las clases de diseño que describen las clases del software y sus operaciones debemos de realizar los contratos de cada una de las operaciones y los diagramas de secuencia de la respuesta a eventos externos. En mucha de las clases sustituiremos esta explicación formal por una más intuitiva e informal para no perder el rumbo didáctico de este temario.

[editar] Diagrama de clases de diseño

Este diagrama, como verás, es diferente al diagramas de clase conceptual ya que será especificará todos los detalles necesarios para la implementación del sistema mediante SDL y C++.

En este diagrama aparecen:

  • Clases: Que participan en las interacciones.
  • Relaciones: Entre las clases y nuevas clases que proporcionan la relaciones entre clases.
  • Atributos: Aparecen todos los atributos necesarios para la implementación de la aplicación.
  • Operaciones: Métodos y funciones necesarias para el desarrollo de la aplicación.

Para presentar estos diagramas vamos a seguir el mismo orden que utilizamos en cuando mostramos los diagramas en la fase de análisis.


El primer subdiagrama que presentamos incluye a las clases que gestionarán los elementos multimedia de la aplicación. Podemos decir que la clase principal de este grupo es la clase Galería que estará compuesta de las clases que están asociadas a ella. El diagrama es el de la figura.


Diagrama de clases (diseño): Componentes multimedia
Diagrama de clases (diseño): Componentes multimedia

En este diagrama se muestran siete clases. Como hemos comentado Galería es la clase que las realaciona a todas. Como puedes observar hemos añadido todo lo necesario para la implementación de las clases. Será en dicho apartado donde estudiaremos todas las nuevas características de las clases. En este momento es importante que observes como han evolucionado las relaciones entre las clases y las propias clases frente a las del análisis.

El segundo diagrama que presentamos relaciona a las clases que nos permiten crear y modificar niveles en la aplicación. En este caso la clase que permite relacionar a las demás componentes del diagrama es la clase Universo aunque la que contendrá toda la lógica para realizar esta tarea es la clase Editor.

Diagrama de clases (diseño): Editando niveles
Diagrama de clases (diseño): Editando niveles

Como puedes observar hemos añadido todo lo necesario para la implementación de las clases. Será en dicho apartado donde estudiaremos todas las nuevas características de las clases. En este momento es importante que observes como han evolucionado las relaciones entre las clases y las propias clases frente a las del análisis.

El tercer diagrama que vamos a estudiar engloba las clases que están asociadas directamente con la clase Participante que recoge todos los elementos activos que participan en los niveles del juego.

Diagrama de clases (diseño): Participantes
Diagrama de clases (diseño): Participantes

Como puedes observar hemos añadido todo lo necesario para la implementación de las clases. Será en dicho apartado donde estudiaremos todas las nuevas características de las clases. En este momento es importante que observes como han evolucionado las relaciones entre las clases y las propias clases frente a las del análisis.

Ahora vamos a estudiar uno de los diagramas de clases más importantes del análisis. Se trata de la clase Universo encargada de relacionar las clases del videojuego, los elementos de control y los elementos multimedia que representa a cada una de estas partes.


Imagen:DcDUNIVERSO.png
Diagrama de clases (diseño): Universo

Como puedes observar hemos añadido todo lo necesario para la implementación de las clases. Será en dicho apartado donde estudiaremos todas las nuevas características de las clases. En este momento es importante que observes como han evolucionado las relaciones entre las clases y las propias clases frente a las del análisis.

Con esta información puedes obtener como es el diagrama general de todas las clases. No se incluye en este temario por problemas de espacio pero puedes encontrarlo en el material del curso.


[editar] Diagrama de secuencia

Estos diagramas van a definir la interacción entre las distintas clases. Vamos a dividir la presentación de estos diagramas por funcionalidad. Debemos de añadir un contrato por cada una de las operaciones que aparecen en los diagramas. Vamos omitir este paso ya que vamos a detallar minuciosamente los detalles del contrato en el aparatdo anterior.

El diseño del videojuego va tomando consistencia. Pronto codificaremos y entenderás mejor todo este proceso.

Diagrama de secuencia (diseño): Universo
Diagrama de secuencia (diseño): Universo


En este diagrama mostramos las interacciones que deben darse entre las principales clases del sistema.

Diagrama de secuencia (diseño): Editor
Diagrama de secuencia (diseño): Editor


En este diagrama presentamos las relaciones que mantiene el Editor con las demás clases del sistema.

Diagrama de secuencia (diseño): Galeria
Diagrama de secuencia (diseño): Galeria


En este diagrama presentamos las relaciones que mantiene la Galería con las demás clases del sistema.

Diagrama de secuencia (diseño): Juego
Diagrama de secuencia (diseño): Juego


En este diagrama presentamos las relaciones que mantiene el Juego con las demás clases del sistema.

Diagrama de secuencia (diseño): Menu
Diagrama de secuencia (diseño): Menu


En este diagrama presentamos las relaciones que mantiene el Menú con las demás clases del sistema.


[editar] Implementación

Una vez realizado el diseño de las clases estamos en condiciones de realizar la implementación de la aplicación. Vamos a presentar cada una de las clases detallando su diseño y mostrando el código resultante del proceso que hemos llevado a cabo. Esta explicación es algo extensa. Recurre a ella si no entiendes alguna de las partes del código de la aplicación.

Hemos realizado una implementación lo más simple posible sin renunciar al compromiso claridad/eficiencia que queremos poner de relieve antes de comenzar con el estudio del videojuego.

[editar] La clase Apuntador

La clase apuntador nace con el objetivo de mostrar el puntero del ratón en el editor de niveles así como para controlar la posición de dicho indicador en todo momento. Además nos va a permitir dar respuesta a las acciones producidas en el ratón sobre las diferentes partes de la pantalla.

El diseño de la clase es::


Clase Apuntador
Clase Apuntador

A partir de este diseño implementamos la clase. La definición de esta clase es:

<cpp> // Listado: Apuntador.h // // La clase Apuntador controla el estado // y la posición del cursor del ratón en la pantalla. // // Haremos uso del cursor del ratón en el Editor de niveles


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


class Editor; // Declaración adelantada


class Apuntador {

public:
   // Constructor
   Apuntador(Editor *editor);
   void dibujar(SDL_Surface *pantalla);
   void actualizar(void);
   // Consultoras
   
   int pos_x(void);
   int pos_y(void);
private:
   // Posición del cursor
   int x, y;
   int bloque_actual;
   
   // Controla la pulsación
   // de los botones del ratón
   int botones;
   // Nos permite asociar el cursor
   // con el editor de niveles
   Editor *editor;
   
   // Discrimina de la zona de pulsación
   void pulsa_sobre_rejilla(int botones);
   void pulsa_sobre_barra(int botones);
   // Indica en que imagen de la rejilla
   // de imágenes auxiliares se posiciona
   // la que representará al cursor
   
   int imagen_rejilla;

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varias variables que nos van a permitir llevar el control sobre el apuntador con el que vamos a trabajar. En las variables x} e y} controlaremos la posición en la que está el ratón en un momento dado. La varíable bloque_actual} nos permite conocer en que bloque, dentro de los recuadros en los que hemos dividio el nivel, ha sido pulsado. Esto es útil para alguno de los métodos de la clase que estudiaremos a continuación. La variable botones} nos permite controlar el estado de los botones del ratón para poder reaccionar a las acciones que llevemos a cabo mediante dicho dispositivo. Mediante la variable editor} asociamos el puntero con las acciones de un editor determinado. Nos queda por describir una variable y dos métodos. Los métodos veremos cual es su función ahora, cuando estudiemos la implementación de la clase. La variable imagen_rejilla} localiza dentro de la rejilla de imágenes auxilares cual utilizamos para representar el cursor del ratón.

En la parte pública de la clase tenemos varias funciones. Los métodos que ofrecemos con esta clase son bastante básicos e intuitivos. Vamos a ver la implementación de dichas funciones:

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

  1. include "CommonConstants.h"
  2. include "Apuntador.h"
  3. include "Nivel.h"
  4. include "Imagen.h"
  5. include "Editor.h"
  6. include "Universo.h"


// Constructor

Apuntador::Apuntador(Editor * editor):

   x(0), y(0), bloque_actual(0), imagen_rejilla(IMAGEN_PUNTERO){


   // Asociamos el apuntador con el editor
   this->editor = editor;

}


void Apuntador::dibujar(SDL_Surface *pantalla){

   // Dibujamos el apuntador en su posición actual
   editor->imagen->dibujar(pantalla, imagen_rejilla, x, y, 1);

}


int Apuntador::pos_x(void) {

   return x;

}


int Apuntador::pos_y(void) {

   return y;

}

void Apuntador::actualizar(void) {

   // Para mantener pulsado el botón del ratón    
   static bool pulsado = false; 
   // Consultamos el estado del ratón
   botones = SDL_GetMouseState(&x, &y);
   // Pulsar sobre la rejilla y algún botón
   if(x < BARRA_HERRAMIENTAS)

// Pulsa sobre la superficie que define el nivel

pulsa_sobre_rejilla(botones);

   else {

// Pulsa en la barra de herramientas

if(botones == SDL_BUTTON(1)) { if(!pulsado) {

pulsa_sobre_barra(botones); pulsado = true;

} } else pulsado = false;

   }

}


void Apuntador::pulsa_sobre_rejilla(int botones) {

   // Con el botón derecho eliminamos el elemento (-1)
   
   if(botones == SDL_BUTTON(3))

editor->nivel->editar_bloque(-1, x, y);


   // Con el izquierdo colocamos el nuevo elemento
   
   if(botones == SDL_BUTTON(1))

editor->nivel->editar_bloque(bloque_actual, x, y); }


void Apuntador::pulsa_sobre_barra(int botones) {

   int fila = y / TAM_TITLE;
   int columna = (x / TAM_TITLE) - 17; // Varía la escala


   // Acciones segúna la pulsación en pantalla
   if(fila == 0) {

switch(columna) {

// Primera fila

case 0:

// Flecha anterior

editor->nivel->anterior(); break;

 	 case 1:

// Icono de guardar

editor->nivel->guardar(); break;


case 2:

// Flecha de siguiente

editor->nivel->siguiente(); break; }

   }
   if(fila == 1) {

switch(columna) {

case 0:

// Recarga el nivel

editor->nivel->cargar(); break;

case 1:

// Sale del editor

editor->universo->cambiar_interfaz(Interfaz::ESCENA_MENU); break;

case 2:

	     // Limpia la escena

editor->nivel->limpiar(); break; }

   }
   // Desplazamiento del scroll
   if(fila == 3 && columna == 1) {

editor->mover_barra_scroll(-1); return;

   }
   if(fila == 14 && columna == 1) {

editor->mover_barra_scroll(+1); return;

   }
   if(fila > 3 && fila < 14)

// Bloque según el icono que pulsemos // en el scroll

bloque_actual = (fila - 4) * 3 + editor->barra_scroll() * 3 + columna;

} </cpp>

El constructor inicializa las variables propias de la clase relacionando al puntero con el editor de niveles. Además inicializamos los valores de la posición e indicamos que recuadro de la rejilla auxiliar de imágenes vamos a usar para representar el puntero del ratón.

El método dibujar utiliza la relación de esta clase con la clase Editor para utilizar el método que nos permite dibujar en pantalla un bitmap. En este caso el método dibuja el apuntador del ratón en la posición a la que hace referencia las variables de la clase.

Los métodos pos_x() y pos_y, como es habitual, devuelven la posición del apuntador del ratón sobre la superficie principal de la aplicación. El método actualizar() nos refresca el estado del ratón. Tomamos el estado del ratón y lo almacenamos en las tres variables de la clase que controlan el estado del ratón. La barra de herramientas del editor de niveles comienza en el punto 544 de ahí la estructura selectiva de este método que nos permite diferenciar el haber pulsado sobre un determinado botón de dicha barra y haber pulsado en la rejilla donde definiremos el nivel.

Vamos con la implementacióń de los métodos privados de la clase. El primero de ellos es pulsa_sobre_barra(). Lo primero que hacemos en este método es calcular en que fila y columnam medidas en número de bloques, dentro de la barra de herramientas estámos situados. Una vez que hemos hecho este cambio de escala tenemos varias estructuras selectivas que van discriminando que botón de dicha barra vamos pulsando. En el caso de la primera fila, segúna la columna, podemos pulsar sobre las flechas que nos permiten navegar por los niveles que hemos creados o en el botón que nos permite guardar las modificaciones hechas en el fichero de niveles.

En el caso de las filas sucesivas vamos actuando según el diseño de la barra de herramientas. Los casos, que podríamos llamar especiales, que nos permiten desplazar la barra donde tenemos almacenadas las iconos que nos permiten añadir elementos al nivel. Para realizar este desplazamiento utilizamos funciones implementadas en la clase editor. Si el elemento pulsado está entre los que componen dichos iconos calculamos el desplazamiento que produce que dicha tira de imágenes esté desplazada.


[editar] La clase Control Animacion

En capítulos anteriores hemos trabajado con esta clase. Se trata de una estructura que nos permite llevar un control de la animación de un personaje tanto para definir la secuencia de cuadros que debe de seguir como establecer un intervalo de tiempo entre que se reproduce un recuadro y el siguiente.

En este caso vamos a utilizar un poco más de la potencia que nos proporciona el lenguaje de programación C++. Vamos a cambiar el vector de bajo nivel que utilizábamos en nuestros ejemplos por un elemento vector de enteros que nos permitirá manejar mejor y más cómodamente los recursos del sistema en cuanto a memoria se refiere.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Control Animacion
Clase Control Animacion

La definición de la clase es la siguiente:

<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_
  1. include <vector>

using namespace std;

class Control_Animacion {

public:
   
   // Constructor
   Control_Animacion(const char *frames, int retardo);
   // Consultoras
   int cuadro(void);
   bool es_primer_cuadro(void);
   
   // Modificadoras
   int avanzar(void);
   void reiniciar(void);
   // Destructor
   ~Control_Animacion();
   
private:
   // Establece el retardo entre 
   // cuadros de la animacion
   int delay;
   // Almacena los cuadros que componen la animacion
   vector<int> cuadros;
   
   // Paso actual de la animación
   // que controlemos con esta clase
   int paso;
   // Contador para llevar un control
   // del retardo
   
   int cont_delay;

};

  1. endif

</cpp>

En la parte privada de la clase definimos variables que nos servirán para controlar la animación. cuadros} es un vector de alto nivel que almacena la secuencia de enteros que define el orden en el que se tienen que mostrar las imágenes con el fin de conseguir la animación deseada. En la variable paso} almacenamos el número de la secuencia en el que nos encontramos mientras que las variables delay} y cont_delay} nos permiten llevar el control de la temporización de la animación.

La implementación de esta clase es la siguiente:

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

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


using namespace std;


Control_Animacion::Control_Animacion(const char *frames, int retardo):

   delay(retardo)

{

   char frames_tmp[MAX_FRAMES];
   char *proximo;
   
   
   strcpy(frames_tmp, frames);
   // Trabajamos con una copia de los cuadros
   // pasados como parámetro
   // Extraemos de la cadena cada uno de los elementos
   for(proximo = strtok(frames_tmp, ","); proximo; ){

this->cuadros.push_back(atoi(proximo)); proximo = strtok(NULL, ",\0");

   }
   // Inicializamos las variables de la clase
   this->cuadros.push_back(-1);
   this->paso = 0;
   this->cont_delay = 0;
  1. ifdef DEBUG
   cout << "Control_Animacion::Control_Animacion()" << endl;
  1. endif

}


int Control_Animacion::cuadro(void) {

   // Devolvemos el paso actual
   // de la animación
   return cuadros[paso];

}


int Control_Animacion::avanzar(void) {

   // Si ha pasado el tiempo suficiente 
   // entre cuadro y cuadro
   if((++ cont_delay) >= delay) {

// Reestablecemos el tiempo a 0

cont_delay = 0;

// Incrimentamos el paso siempre // que no sea el último elemento // lo que hará que volvamos al // primer elemento

if(cuadros[++paso] == -1) { paso = 0; return 1; }

   }
   return 0;

}


void Control_Animacion::reiniciar(void) {

   // Volvemos al inicio de la animación
   paso = 0;
   cont_delay = 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>

En el constructor de la clase incializamos las variables propias de esta clase así como descomponemos la cadena que recibe como parámetro con el fin de almacenar la secuencia que recibimos como parámetro en el vector que controlará la animación. El método cuadro() devuelve el frame que corresponde con el momento actual de la animación. La función miembro avanzar() pasa al siguiente cuadro de la animación siempre que haya pasado el tiempo suficiente. Esta es una forma de controlar la temporalidad que nos permite dar un comportamiento exclusivo a cada animación de las que posea un determinado personaje.

El método reiniciar() nos permite poner la animación a su estado incial sin tener que volver a crearla y el método es_primer_cuadro() nos permite consultar si la animación se encuentra en dicho estado. El destructor no realiza ninguna tarea especial.


[editar] La clase Control Juego

Esta es una de las clases más importantes del desarrollo del videojuego. Nos permite llevar un control de la lógica del juego y de todos los personajes que están involucrados en el mismo. El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Control Juego
Clase Control Juego

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Control_Juego.h // // La clase Control Juego proporcina un mando lógico sobre el juego //


  1. ifndef _CONTROL_JUEGO_H_
  2. define _CONTROL_JUEGO_H_
  1. include <SDL/SDL.h>
  2. include <list>
  3. include "Participante.h"


// Declaración adelantada

class Protagonista; class Enemigo; class Item; class Juego;


using namespace std;


class Control_Juego {

public:
   
   // Constructor
   Control_Juego(Juego *juego);
   
   // Para realizar la actualización lógica 
   // y poder mostrar el resultado en pantalla
   
   void actualizar(void);
   void dibujar(SDL_Surface *pantalla);
   
   Juego *juego;
   
   // Nos permiten establecer a los participantes
   // del juego en el universo del juego
   void enemigo(Participante::tipo_participantes tipo, int x, int y, int flip);
   void protagonista(int x, int y, int flip);
   void item(Participante::tipo_participantes tipo, int x, int y, int flip);
   
   // Destructor
   ~Control_Juego();
private:
   void avisar_colisiones(void);
   // Galería de personajes y objetos del juego
   
   Protagonista *protagonista_;
   list<Enemigo *> lista_enemigos;
   list<Item *> lista_items;
   // Detección de colisiones
   
   bool hay_colision(int x0, int y0, int x1, int y1, int radio);
   void eliminar_antiguos_items(list<Item *>& lista);
   void eliminar_antiguos_enemigos(list<Enemigo *>& lista);

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varios elementos. Los atributos lista_enemigos} y lista_items} son listas que nos permiten llevar un control de los objetos y enemigos presentes en el nivel mientras que con protagonista} realizamos la misma tarea con el personaje principal del juego. Las listas han sido definidas sobre list} de la STL} que nos permitiran un manejo sencillo de este tipo de estructuras.

En esta parte de la clase definimos también varias funciones que vamos a necesitar para controlar la aplicación. avisar_colisiones()} hace un recorrido por todos los items y adversarios para comprobar si existe colisión con el personaje principal. Con esto cubrimos todas las posibles colisiones a las que tenemos que reaccionar. La función hay_colision()} es la encargada de la detección de colisiones en el juego por lo que es de vital importancia.

Las funciones eliminar_antiguos()} se encargan de borrar aquellos elementos del juego que por un motivo o por otro estén en estado de eliminar. Esto suele ocurrir cuando un personaje muere o un ítem ha colisionado con el personaje principal del juego.

En la parte pública de la clase tenemos varios métodos, mucho de ellos comunes a la mayoría de las clases. El constructor de la clase, las funciones actualizar()} y dibujar()}. La primera actualiza el estado lógico de todos los componentes del juego mientras que la segunda los dibuja en determinados momentos. Tenemos tres método (enemigo()}, protagonista()} y item()}) que nos permiten agregar cada uno de los elementos asociados a éstos a la aplicación. En enlace de dependencia con la clase Juego} es público ya que necesitaremos tener acceso desde otras clases a través del Control del Juego.

Hemos realizado un recorrido rápido por los métodos y atributos clase, veamos ahora como se ha implementado dicha clase:

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


  1. include <iostream>
  1. include "Control_Juego.h"
  2. include "Juego.h"
  3. include "Item.h"
  4. include "Enemigo.h"
  5. include "Protagonista.h"
  6. include "Enemigo.h"
  7. include "Universo.h"


using namespace std;


Control_Juego::Control_Juego(Juego *juego): protagonista_(NULL) {

  1. ifdef DEBUG
   cout << "Control_Juego::Control_Juego()" << endl;
  1. endif
   this->juego = juego;

}


void Control_Juego::actualizar(void) {


   // Actualizamos el protagonista
   if(protagonista_)

protagonista_->actualizar();

   // Actualizamos todos los enemigos    
   for(list<Enemigo *>::iterator i = lista_enemigos.begin();

i != lista_enemigos.end(); ++i) {

(*i)->actualizar();

   }


   // Actualizamos todos los items
   
   for(list<Item *>::iterator i = lista_items.begin();

i != lista_items.end(); ++i){

(*i)->actualizar();

   }


   // Eliminamos los antiguos, marcados con ELIMINAR
   eliminar_antiguos_enemigos(lista_enemigos);
   eliminar_antiguos_items(lista_items);
   // Estudiamos las posibles colisiones
   if(protagonista_ != NULL)

avisar_colisiones();


}


void Control_Juego::dibujar(SDL_Surface *pantalla) {


   // Dibujamos toda la lista de enemigos
   for(list<Enemigo *>::iterator i = lista_enemigos.begin();

i != lista_enemigos.end(); ++i){

(*i)->dibujar(juego->universo->pantalla);

   }
   // Dibujamos al protagonista
 
   if(protagonista_)

protagonista_->dibujar(juego->universo->pantalla);


   // Dibujamos toda la lista de items
   
   for(list<Item *>::iterator i = lista_items.begin();

i != lista_items.end(); ++i){

(*i)->dibujar(juego->universo->pantalla);

   }


}


void Control_Juego::avisar_colisiones(void) {


   static int radio = 20; 
   int x0, y0;
   int x1, y1;
   // Tomamos la posición del protagonista
   x0 = protagonista_->pos_x();
   y0 = protagonista_->pos_y();
   
   // Comprobamos si hay colisión con los enemigos
   for(list<Enemigo *>::iterator i = lista_enemigos.begin();

i != lista_enemigos.end(); ++i) {

// Posición del enemigo

x1 = (*i)->pos_x(); y1 = (*i)->pos_y();

// Comprobamos si hay colisión entre el elemento y el protagonista

if(hay_colision(x0, y0, x1, y1, radio)) {

if(protagonista_->estado_actual() != Participante::GOLPEAR) {

// Mata al protagonista

protagonista_->colisiona_con((*i));

} else {

// Muere el malo

(*i)->colisiona_con(protagonista_); // Elimina el enemigo

} }

   }


   // Comprobamos si hay colisión con los objetos
   for(list<Item *>::iterator i = lista_items.begin();

i != lista_items.end(); ++i) {

// Posición del item

x1 = (*i)->pos_x(); y1 = (*i)->pos_y();


// Comprobamos si hay colisión entre el elemento y el protagonista

if(hay_colision(x0, y0, x1, y1, radio)) {

(*i)->colisiona_con(protagonista_); // Elimina el ítem

return;

}

   }


}


// Creando los enemigos

void Control_Juego::enemigo(Participante::tipo_participantes tipo, int x, int y, int flip) {


  1. ifdef DEBUG
   cout << "Creando un enemigo tipo " << tipo << "en ( " << x << " - "

<< y << " )" << endl;

  1. endif


   // Almacenamos en las listas el tipo enemigo que queremos crear
   switch(tipo) {
    case Participante::TIPO_ENEMIGO_RATA:

lista_enemigos.push_back(new Enemigo(Participante::TIPO_ENEMIGO_RATA, juego, x, y, flip)); break;

    case Participante::TIPO_ENEMIGO_MOTA:

lista_enemigos.push_back(new Enemigo(Participante::TIPO_ENEMIGO_MOTA, juego, x, y, flip)); break;

    default:

cerr << "Enemigo desconocido" << endl;

   }


}


void Control_Juego::protagonista(int x, int y, int flip) {


   if(protagonista_ != NULL)

cerr << "Ya existe un protagonista en el nivel" << endl;

   else

protagonista_ = new Protagonista(juego, x, y, flip);


}


void Control_Juego::item(Participante::tipo_participantes tipo, int x, int y, int flip) {


  1. ifdef DEBUG
   cout << "Creando un item en ( " << x << " - "

<< y << " )" << endl;

  1. endif
   // Almacenamos en las listas el tipo de item que queremos crear
   switch(tipo) {
    case Participante::TIPO_DESTORNILLADOR:

lista_items.push_back(new Item(Participante::TIPO_DESTORNILLADOR, juego, x, y, flip)); break;

    case Participante::TIPO_ALICATE:

lista_items.push_back(new Item(Participante::TIPO_ALICATE, juego, x, y, flip)); break;

    default:

cerr << "Item desconocido" << endl; return;

   }


}


Control_Juego::~Control_Juego() {

   // Borramos las estructuras activas del juego
   delete protagonista_;


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

}

bool Control_Juego::hay_colision(int x0, int y0, int x1, int y1, int radio) {


   if((abs(x0 - x1) < (radio * 2)) &&(abs(y0 - y1) < (radio *2)))
       return true;
   else
       return false;


}

void Control_Juego::eliminar_antiguos_items(list<Item *>& lista) {

   list<Item *>::iterator i;
   bool eliminado = false;


   // Eliminamos los items marcados como eliminar
   for(i = lista.begin(); i != lista.end() && eliminado == false; ++i) {

if((*i)->estado_actual() == Participante::ELIMINAR) {

lista.erase(i); // más eficiente que remove(*i) eliminado = true;

}

   }

}

void Control_Juego::eliminar_antiguos_enemigos(list<Enemigo *>& lista) {

   list<Enemigo *>::iterator i;
   bool eliminado = false;
   // Eliminamos los items cuyo estado sea eliminar
   for(i = lista.begin(); i != lista.end() && eliminado == false; ++i) {

if((*i)->estado_actual() == Participante::ELIMINAR) {

lista.erase(i); eliminado = true;

}

   }

} </cpp>

Vamos a estudiar aquellos métodos que necesiten un mayor detalle. El método actualizar() hace una llamada a los métodos actualizar() de cada uno de los componentes que integran el nivel en el que nos encontremos. Esta operación es de O(n) ya que tenemos que recorrer todos los elementos de las listas donde están almacenados dichos elementos. Para realizar estos recorridos hemos hecho uso de los iteradores propios de cada tipo de datos. Este es un tipo de dato que no habíamos utilizado hasta ahora por lo que damos un paso más en la utilización de C++ en este tutorial. Una de las utilidades de los iteradores es recorrer tipos de datos que no podemos indexar como un vector. Para ello definimos un iterador del tipo de la estructura a recorrer y definimos dicho recorrido desde el comienzo de la secuencia (lista.begin()), hasta el final de la mismta (lista.end()) incrementando en una unidad dicho iterador por cada vuelta del bucle. Como puedes ver no es un concepto complicado pero si es muy útil para manejar las secuencias que nos proporciona la STL. Al final de este método hacemos una llamada a la función eliminar_antiguos() que elminará aquellos elementos con tenga el estado ELIMINAR debido a alguna acción del juego.


[editar] La clase Control Movimiento

La clase Control Movimiento mueve determinados elementos de la aplicación dotándolos de un efecto especial. En nuestro caso los utilizamos en el menú de la pantalla principal para que los textos aparezcan mediante un efecto de movimiento progresivo.

El diseño de la clase del que partimos para la implementación es el de la figura.


Clase Control Movimiento
Clase Control Movimiento


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Control_Movimiento.h // // La clase Control Movimiento controla la animación externa de los personajes // lo que es lo mismo, el movimiento sobre el mapa del nivel


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


class Imagen; // Declaración adelantada


class Control_Movimiento {

public:
   //Constructor
   Control_Movimiento(Imagen *imagen, int indice = 0, int x = 0, int y = 0);
   // Modificadoras
   void actualizar(void);
   void dibujar(SDL_Surface *pantalla);
   void mover(int x, int y);
   void mover_inmediatamente(int x, int y);
   
private:
   
   // Posición
   int x, y;
   // Destino
   int x_destino, y_destino;
   // Dentro de la rejilla especificamos qué imagen
   int indice;
   // Rejilla de imágenes
   Imagen *imagen;

};

  1. endif

</cpp>

En la parte privada de la clase tenemos dos variables x e y que nos permiten almacenar la posición actual del elemento a mover así como x_destino e y_destino nos permiten especificar a dónde queremos realizar el movimiento lógico de la imagen que controla esta clase. Para saber qué imagen vamos a mover la clase realiza una asociación mediante una variable imagen de tipo Imagen y utiliza un indicador almacenado en la variable indice que nos permite seleccionar un determinado cuadro de una rejilla.

En la parte pública tenemos varios métodos que vamos a estudiar con la implementación de la clase que vemos a continuación.

<cpp> // Listado: Control_Movimiento.cpp // // Implementación de la clase Control Movimiento


  1. include <iostream>
  1. include "Control_Movimiento.h"
  2. include "Imagen.h"

using namespace std;

Control_Movimiento::Control_Movimiento(Imagen *imagen, int indice, int x, int y) {

   // Inicializamos los atributos de la clase
   this->imagen = imagen;
   this->x = x;
   this->y = y;
   x_destino = x;
   y_destino = y;
   this->indice = indice;
   
  1. ifdef DEBUG
   cout << "Control_Movimiento::Control_Movimiento()" << endl;
  1. endif

}


void Control_Movimiento::actualizar(void) {

   // Actualización de la posición del elemento
   int dx = x_destino - x;
   int dy = y_destino - y;
   
   // Si hay movimiento
   
   if(dx != 0) {

// Controlamos la precisión del movimiento

if(abs(dx) >= 4) // Si es mayor o igual que 4 x += dx / 4; // la reducimos else x += dx / abs(dx); // Si es menor, reducimos a 1

   }
   if(dy != 0)	{

// Ídem para el movimiento vertical

if(abs(dy) >= 4) y += dy / 4; else y += dy / abs(dy);

   }

}


void Control_Movimiento::dibujar(SDL_Surface *pantalla) {

   imagen->dibujar(pantalla, indice, x, y, 1);

}


void Control_Movimiento::mover(int x, int y) {

   // Este movimiento es actualizado por la función actualizar()
   // de esta clase con la precisión que tenemos implementada
   // en ella
   x_destino = x;
   y_destino = y;

}


void Control_Movimiento::mover_inmediatamente(int x, int y) {

   // Esta función nos permite hacer un movimiento inmediato
   // a una determinada posición
   mover(x,y);
   this->x = x;
   this->y = y;

} </cpp>

El primero que vemos es el constructor de la clase que se encarga de inicializar los atributos propios de la clase. El método actualizar() desplaza la imagen que controla reduciendo la velocidad de aproximación si se encuentra cerca de la posición de destino. Para conseguir este efecto vamos comprobando si el diferencial de distancia entre la posición actual y la de destino es menor que cuatro se reduce a la unidad mientras que si es mayor reducidmos dicha distancia en una cuarta parte. Este movimiento es aplicado tanto en el eje horizontal como en el vertical.

El método dibujar() muestra por pantalla el cuadro específico de la imagen asociada mediante un método de la clase imagen. El método mover() establece las variables para que el método actualizar se encargue de realizar el movimiento lógico de la imagen mientras que el método mover_inmediatamente fija mueve dicha imagen sin realizar el efecto, simplemente colocándola en el lugar correspondiente.


[editar] La clase Editor

La clase Editor controla todo lo referente al interfaz del editor de niveles que nos permitirá construir los distintos niveles para nuestro videojuego. Este editor dispone de una superficie editable dónde poder realizar las modificaciones y una barra de herramientas con los elementos que podemos insertar en dicha área. Además todas las modiciaciones las realizaremos con el ratón por lo que dispone de un dispositivo apuntador asociado a él.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Editor
Clase Editor


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Editor.h // // Controla las características del editor de niveles

  1. ifndef _EDITOR_H_
  2. define _EDITOR_H_
  1. include <SDL/SDL.h>
  2. include "Interfaz.h"

// Declaraciones adelantadas

class Universo; class Nivel; class Teclado; class Imagen; class Apuntador;

// Definición de la clase

class Editor: public Interfaz {

public:
   // Constructor
   Editor(Universo *universo);
   // Acciones de la clase
   
   void reiniciar(void);
   void actualizar(void);
   void dibujar(void);
   
   // Barra de menú
   void mover_barra_scroll(int incremento);
   int barra_scroll(void);
   // Destructor
   ~Editor();
   
   Nivel *nivel;
   Teclado *teclado;
   Imagen *imagen;
   
private:
   int x, y;
   int barra_scroll_;
   
   Apuntador *apuntador;
   
   void mover_ventana(void);
   void dibujar_menu(void);
   void dibujar_numero_nivel(void);

};

  1. endif

</cpp>

En la parte privada de la clase tenemos tres atributos. El atributo apuntador asocia un dispositivo apuntador al editor para poder realizar las modificaciones en el área editable. El atributo barra_scroll_ nos permite controlar si el área donde se ha realizado un click forma parte de la barra de herramientas o de la superficie editable.

Además de estos atributos en la parte privada de la clase tenemos tres métodos que definen parte del comportamiento del editor. El primero de ellos, mover_ventana(), posiciona la ventana segúna el lugar (x, y) donde se encuentre el apuntador del ratón. El método dibujar_menu() se encarga de mostrar en pantalla la barra de herramientas que nos permite seleccionar las herramientas para editar el nivel. El último método de esta parte, dibujar_numero_nivel() se encarga de mostrarnos en pantalla el número de nivel que estamos editando.

En la parte pública de la clase tenemos varios métodos que vamos a estudiar con la implementación de la clase:

<cpp>Editor</cpp>

El primero de los métodos es el constructor de la clase. En este método inicializamos el nivel que vamos a editar definiendo la zona que va a ser editable de la pantalla. Asociamos un teclado y un ratón al editor. Inicializamos los atributos de la clase y cargamos la imagen que contienen los reacuadros o tiles que nos van a servir para rellenar las partes del nivel que queramos dotar de un cierto comportamiento.

El método reiniciar() reestablece la música del editor. El método actualizar() se encarga de que la posición de la ventana que nos permite navegar por el nivel sea la correcta así como de actualizar la posición del apuntador del ratón y del estado del nivel que estamos editando. Si pulsamos la tecla de salida este método se encarga de que salgamos del editor al menú principal.

El método mover_barra_scroll() mueve dicha barra siempre que el desplazamiento sea mayor que 5 píxeles. mover_ventana() como puedes observar mueve la ventana veinte píxeles hacia el lugar que hayamos indicado por medio del teclado y los métodos dibujar...() muestran por pantalla todas las imágenes que componen el editor.


[editar] La clase Enemigo

La clase enemigo nos permite crear adversarios en el videojuego. Esta clase es hija de la clase participante y tiene una implementación muy básica.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Enemigo
Clase Enemigo


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Enemigo.h // // Clase Enemigo, heredada de Participante, para el control // de los adversarios del juego

  1. ifndef _ENEMIGO_H_
  2. define _ENEMIGO_H_
  1. include "Participante.h"

class Juego;

class Enemigo : public Participante {

public:
   
   // Constructor
   Enemigo(enum tipo_participantes tipo, Juego *juego, int x, int y, int flip = 1);
   // Modificadora
   void actualizar(void);
   // Consultora
   void colisiona_con(Participante *otro);
   virtual ~Enemigo();

};


  1. endif

</cpp>

Todos los elementos que definimos en esta clase son públicos ya que los privados son heredados. Todos los elementos de los que disponemos son métodos que nos permiten interactuar con los enemigos. Vamos a ver la implementación de estos métodos y vamos a estudiar cada uno de ellos:

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

  1. include <iostream>
  1. include "Enemigo.h"
  2. include "Juego.h"
  3. include "Universo.h"
  4. include "Galeria.h"
  5. include "Sonido.h"
  6. include "Imagen.h"
  7. include "Control_Animacion.h"
  8. include "Nivel.h"


using namespace std;


Enemigo::Enemigo(enum tipo_participantes tipo, Juego *juego, int x, int y, int direccion):

   Participante(juego, x, y, direccion) {
  1. ifndef DEBUG
   cout << "Enemigo::Enemigo()" << endl;
  1. endif
   // Creamos las animaciones para el personaje
   animaciones[PARADO] = new Control_Animacion("0", 5);
   animaciones[CAMINAR] = new Control_Animacion("0,1,2", 5);
   animaciones[MORIR] = new Control_Animacion("1,2,1,2,1,2", 1);
   
   // Según el tipo de enemigo que estemos creando
   switch(tipo) {

    case TIPO_ENEMIGO_RATA:

imagen = juego->universo->galeria->imagen(Galeria::ENEMIGO_RATA); break;

    case TIPO_ENEMIGO_MOTA:

imagen = juego->universo->galeria->imagen(Galeria::ENEMIGO_MOTA); break;

   default:

cout << "Enemigo::Enemigo() -> Enemigo no contenplado" << endl;

   }
   // Estado inicial para los enemigos
   estado = CAMINAR;

}


void Enemigo::actualizar(void) {

   if(estado == MORIR) {

if(animaciones[estado]->avanzar()) estado = ELIMINAR;

   }
   else

// Hacemos avanzar la animación animaciones[estado]->avanzar();

   // Tiene que llegar al suelo
   velocidad_salto += 0.1;
   y += altura((int) velocidad_salto);
   if(pisa_el_suelo()) {

// Hacemos que gire si se acaba el suelo

if(!pisa_el_suelo( x + direccion, y)) direccion *= -1;

x += direccion;

   }
   

}

void Enemigo::colisiona_con(Participante *otro) {

   // Si colisiona y el personaje principal está golpeando
   // muere para desaparecer
   if(estado != MORIR) {

juego->universo->\ galeria->sonidos[Galeria::MATA_MALO]->reproducir();

estado = MORIR;

   }


}


Enemigo::~Enemigo() {

  1. ifdef DEBUG
   cout << "Enemigo::~Enemigo" << endl;
  1. endif

} </cpp>

El constructor de la clase se encarga de inicializar todos los métodos de la clase. Lo primero que realizamos es la inicialización de las animaciones que van a responder a los diferentes estados del personaje según transcurra el videojuego. Seguidamente, según sea el tipo de enemigo que queremos utilizar, cargamos de la galería una imágenes u otras. El estado inicial para los enemigos será el de caminar.

El siguiente método es el de actualizar(). Lo primero que hace este método es comprobar que el enemigo no ha muerto. De ser así se avanza en la animación y se marca su estado con la etiqueta ELIMINAR para que al actualizar la lógica del juego haga desaparecer este elemento de la pantalla. Si el enemigo no ha muerto todavía el método hace avanzar la animación del enemigo. En caso de no estar en el suelo se le hace caer con una velocidad que aumenta según vaya cayendo el personaje.

El método colisiona_con() define que acciones han de realizarse cuando se produce una colisión con el elemento que recibe como parámetro. En este caso las colisiones sólo pueden producirse con el personaje principal. En el caso de que se informe a un objeto de esta clase de una colisión esta provocará que se pase el elemento al estado MORIR para que se reproduzca la animación y sea eliminado.


[editar] La clase Fuente

La clase Fuente nos permite dibujar en una superficie SDL cualquier una palabra o un texto que queramos mostrar a partir de una fuente TTF o de una rejilla de letras tipográficas.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Fuente
Clase Fuente

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Fuente.h // // Esta clase controla la impresión de textos en pantalla // mediantes fuentes ttf o tira de imágenes


  1. ifndef _FUENTE_H_
  2. define _FUENTE_H_
  1. include <SDL/SDL.h>
  2. include <SDL/SDL_image.h>
  3. include <SDL/SDL_ttf.h>
  1. include "CommonConstants.h"


class Fuente {

public:
   // Constructores
   Fuente(const char *path, int tamano = 20);    // Fuente ttf
   Fuente(const char *path, bool alpha = false); // Fuente ya renderizada


   // Funciones para escribir sobre una superfice
   void dibujar(SDL_Surface *superficie, char *cadena, int x, int y);


   // Funciones para escribir utilizando una fuente ttf
   void dibujar_palabra(SDL_Surface *superficie, \

char *palabra, int x, int y, SDL_Color color);

   void dibujar_frase(SDL_Surface *superficie, \

char *frase, int x, int y, SDL_Color color);

   // Destructor
   ~Fuente();
private:
   TTF_Font *fuente;          // Para manejar la fuente TTF
   SDL_Surface *imagen_texto; // Imagen obtenida a partir de la fuente
   // Tamaño de la fuente
   int puntos;


   // Dónde almacenar la imagen resultante para mostrar		
   SDL_Surface *imagen;
   int separacion; // entre las letras


   // Define el orden de las letras en la rejilla-imagen
   char cadena[113 + 1];


   // Mantiene la posición de cada letra en la rejilla-imagen
   SDL_Rect rect_letras[113];
   
   void buscar_posiciones(void);


   // Dibuja una letra única
   int dibujar(SDL_Surface *superficie, char letra, int x, int y);
   
   // Toma un píxel de una superficie
   Uint32 tomar_pixel(SDL_Surface *superficie, int x, int y);
   

};


  1. endif

</cpp>

En la parte privada de la clase tenemos varios elementos. El primero es una variable de tipo TTF_Font que nos servirá para hacer uso de la librería SDL_ttf y el segundo una variable de tipo SDL_Surface por si optamos por la utilización de la rejilla de letras tipográficas. El atributo puntos sirve para almacenar el tamaño de la fuente a utilizar. Los métodos privados de la clase los estudiaremos cuando veamos la implementación de la misma.

En la parte pública de la clase se ofrecen varios métodos. Disponemos de dos constructores según el método que vayamos a utilizar. El método dibujar() que nos permite escribir una cadena en una superficie SDL y dos métodos auxiliares de éste que son dibujar_palabra() y dibujar_frase() que son una especificación del método dibujar().

Para saber cómo y qué hace cada método vamos a estudiar la implementación de la clase.

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

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


using namespace std;


// Mediante fuente ttf

Fuente::Fuente(const char *path, int tamano) {

   // Iniciamos la librería SDL_ttf
   if(TTF_Init() < 0) {

cerr << "No se puede iniciar SDL_ttf" << endl; exit(1);

   }
   
   // Al terminar cerramos SDL_ttf
   atexit(TTF_Quit);


   // Tamaño en puntos de la fuente
   puntos = tamano;
   
   // Cargamos la fuente que queremos utilizar con un determinado tamaño
   fuente = TTF_OpenFont(path, tamano);
   if(fuente == NULL) {

cerr << "No se puede abrir la fuente deseada" << endl; exit(1);

   }

}


// Mediante fuentes en una rejilla-imagen

Fuente::Fuente(const char *path, bool alpha) {

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


   // Cargamos la imagen que contiene las letras 
   // renderizadas a utilizar
   imagen = IMG_Load(path);
   if(imagen == NULL) {

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

   }
   if(!alpha) {

// A formato de imagen

SDL_Surface *tmp = imagen;

imagen = SDL_DisplayFormat(tmp); SDL_FreeSurface(tmp);

if(imagen == NULL) {

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

}

// Establecemos el color key

Uint32 colorkey = SDL_MapRGB(imagen->format, 255, 0, 255); SDL_SetColorKey(imagen, SDL_SRCCOLORKEY, colorkey);

   }
   buscar_posiciones();

}



void Fuente::dibujar_palabra(SDL_Surface *superficie, char *palabra, \ int x, int y, SDL_Color color) {

   // Renderizamos y almacenamos en una superficie
   imagen = TTF_RenderText_Solid(fuente, palabra, color);


   // Convertimos la imagen obtenida a formato de pantalla
   SDL_Surface *tmp = imagen;
   imagen = SDL_DisplayFormat(tmp);
   SDL_FreeSurface(tmp);


   // Colocamos en la pantalla principal en(x, y)
   SDL_Rect destino;
   destino.x = x;
   destino.y = y;
   destino.w = imagen->w;
   destino.h = imagen->h;
   SDL_BlitSurface(imagen, NULL, superficie, &destino);
   

}


// Construimos frases con fuentes ttf cuyo espacio no sea compatible // con SDL_ttf

void Fuente::dibujar_frase(SDL_Surface *superficie, char *frase, \ int x, int y, SDL_Color color) {

   int offset_x = 0; // Distancia entre una letra y la siguiente
   // Separamos la frase en palabras
   int k = 0;
   
   // Mientras no termine la cadena
   while(frase[k] != '\0') {

int i = 0; char palabra[MAX_LONG_PAL];

for(int j = 0; j < MAX_LONG_PAL; j++) palabra[j] = '\0';

// Por si empieza por espacio

while(frase[k] == ' ') k++;

// Hasta encontrar un espacio o el final de la cadena

while(frase[k] != ' ' && frase[k] != '\0') { palabra[i] = frase[k]; i++; k++; }

if(frase[k] != '\0') k++;


// No utilizamos la función anterior para facilitar el // cálculo del offset

// Renderizamos y almacenamos en una superficie

imagen = TTF_RenderText_Solid(fuente, palabra, color);


// Convertimos a formato de pantalla

SDL_Surface *tmp = imagen; imagen = SDL_DisplayFormat(tmp); SDL_FreeSurface(tmp);


// Colocamos en la pantalla principal en (x, y)

SDL_Rect destino;

destino.x = x + offset_x; destino.y = y; destino.w = imagen->w; destino.h = imagen->h;

// Calculamos el offset entre letras

offset_x = offset_x + imagen->w + puntos / 3;

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

   }

}


Fuente::~Fuente() {

   // Liberamos la superficie
   SDL_FreeSurface(imagen);

}


void Fuente::dibujar(SDL_Surface *superficie, char *texto, int x, int y) {

   int i;
   int aux = 0;
   for(i = 0; texto[i] != '\0'; i ++) {

// Dibujamos carácter a carácter la frase

aux = dibujar(superficie, texto[i], x, y); x = x + aux + 2;

   }

}


int Fuente::dibujar(SDL_Surface *superficie, char letra, int x, int y) {

   static int dep = 0;
   if(letra == ' ') {

// Espacio en la rejilla

x += 16; return 16;

   }
   bool encuentra = false;
   int i;
   // Buscamos si disponemos de representación gráfica para el carácter
   for(i = 0; cadena[i] != '\0' && encuentra == false; i++) {

if(cadena[i] == letra) encuentra = true;

// Realiza un incremento más de i

   }
   i--; // i era el '\0'


   // Comprobamos si el carácter ha sido encontrado
   if(encuentra == false && dep == 0) {

cerr << "No se encuentra el caracter: " << letra << endl; dep = 1;

return 0;

   }
   SDL_Rect destino; // Lugar donde vamos a posicionar la letra
   destino.x = x;
   destino.y = y;
   destino.w = rect_letras[i].w;
   destino.h = rect_letras[i].h;
   // Hacemos el blitting sobre la superficie
   SDL_BlitSurface(imagen, &rect_letras[i], superficie, &destino);
   // Devolvemos el ancho de la letra
   return rect_letras[i].w;

}


void Fuente::buscar_posiciones(void) {

   const int LEYENDO = 0;
   const int DIVISION = 1;
   int indice = 0;
   Uint32 color = 0;
   Uint32 negro = SDL_MapRGB(imagen->format, 0, 0, 0);
   int estado = DIVISION;


   strcpy(cadena, "abcdefghijklmnopqrstuvwxyz"			\

"ABCDEFGHIJKLMNOPQRSYUVWXYZ" \ "1234567890" \ "ñÑáéíóúÁÉÍÓÚäëïöü" \ "!¡?¿@#$%&'+-=><*/,.:;-_()[]{}|^`~\\" );

   if(SDL_MUSTLOCK(imagen)) {

if(SDL_LockSurface(imagen) < 0) {

cerr << "No se puede bloquear " << imagen << " con " << "SDL_LockSurface" << endl; return;

}

   }
   indice = -1;

   for(int x = 0; x < imagen->w; x ++) {

color = tomar_pixel(imagen, x, 0);

if(estado == DIVISION && color == negro) {

estado = LEYENDO;

indice++;

rect_letras[indice].x = x; rect_letras[indice].y = 2; rect_letras[indice].h = imagen->h - 2; rect_letras[indice].w = 0;

}

if(color == negro) rect_letras[indice].w++; else estado = DIVISION;

   }
   if(SDL_MUSTLOCK(imagen))

SDL_UnlockSurface(imagen); }


// Fuente: SDL_Doc

Uint32 Fuente::tomar_pixel(SDL_Surface *superficie, int x, int y) {

   int bpp = superficie->format->BytesPerPixel;
   Uint8 *p =(Uint8 *)superficie->pixels + y * superficie->pitch + x * bpp;
   
   switch(bpp)  {
    case 1:

return *p;

    case 2:

return *(Uint16 *)p;

    case 3:

if(SDL_BYTEORDER == SDL_BIG_ENDIAN) return p[0] << 16 | p[1] << 8 | p[2]; else return p[0] | p[1] << 8 | p[2] << 16;

    case 4:

return *(Uint32 *)p;

   default:

return 0;

   }

} </cpp>

El primer constructor utiliza una fuente ttf y SDL_ttf para proporcionar un texto en una superficie. Lo primero que hace el constructor es iniciar la citada librería ya que depende de ella. Una vez realizada esta tarea inicializa los atributos de la clase referentes a esta alternativa de trabajo.

El segundo constructor utiliza una rejilla de carácteres para mostrar un texto en pantalla. La idea es simple. Se prepara un BMP con el alfabeto listo para escoger una posición de la rejilla y mostrar una letra. Repitiendo esta acción podemos construir cualquier texto con unas fuentes personalizadas. Lo primero que hace este constructor es cargar la imagen donde está almacenada dichar rejilla. Seguidamente preparamos la imagen para que esté en el mismo formato que la superficie principal y establecemos el color key que utilizamos para toda la aplicación. Para terminar el constructor hace una llamada a buscar_posiciones(). Esta función es privada a esta clase y se encarga de estabelcer una relación entre la rejilla de imágenes y un vector de carácteres que nos permitirá seleccionar más ágilmente las letras que queremos colocar sobre una superficie.

La clase ofrece dos métodos para dibujar texto. El primero dibuja un texto en una superficie haciendo uso del otro método que nos permite dibujar una sóla letra en dicha superficie. Dibujando letra tras letra podemos dibujar un texto, como no podía ser de otra forma.

Para terminar la clase implementa un método de la librería de SDL para comprobar que la letra que está tomando de la rejilla es una imagen y no un píxel de división de letras.


[editar] La clase Galeria

La clase Galería es una de las más importantes de nuestra aplicación. Gestiona todos los elementos multimedia que vamos a utilizar en el videojuego por lo que su implementación es crucial. Esta galería almacena cuatro tipo de elementos: imágenes, sonidos, fuentes y música. El fin de esta galería es tener una gestión controlada de dichos elementos que pueden utilizarse para muy diferentes fines.

Para cada uno de los elementos de la clase tenemos implementada una clase que nos permite envolver a cada elemento ofreciendo una mayor potencia que si almacenásemos el elemento en crudo.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Galeria
Clase Galeria

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Galeria.h // // Controla los elementos multimedia de la aplicación

  1. ifndef _GALERIA_H_
  2. define _GALERIA_H_
  1. include <map>
  2. include "CommonConstants.h"


// Declaración adelantada

class Imagen; class Fuente; class Musica; class Sonido;


using namespace std;

class Galeria {

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

TILES, PERSONAJE_PPAL, ENEMIGO_RATA, ENEMIGO_MOTA, MENU, TITULO_TUTORIAL, TITULO_FIRMA, ITEM_ALICATE, ITEM_DESTORNILLADOR

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

FUENTE_MENU

   };
   enum codigo_musica { 

MUSICA_MENU, MUSICA_EDITOR, MUSICA_JUEGO

   };
   enum codigo_sonido {

COGE_ITEM, MATA_MALO, PASA_NIVEL, EFECTO_MENU, MUERE_BUENO

   };


   // Constructor
   Galeria ();
   // Consultoras		

   Imagen *imagen(codigo_imagen cod_ima);
   Fuente *fuente(codigo_fuente indice);
   Musica *musica(codigo_musica cod_music);
   Sonido *sonido(codigo_sonido cod_sonido);
   ~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;
   map<codigo_musica, Musica *> musicas;
   map<codigo_sonido, Sonido *> sonidos;

};

  1. endif

</cpp>

Como puedes ver la definición de la clase es bastante sencilla. Tenemos unos enumerados para cada tipo de dato que vamos a almacenar que nos permiten trabajar más cómodamente a la hora de implementar el código del videojuego.

La clase ofrece varios métodos que nos permiten obtener cada uno de los componentes sólo con pasarle como parámetro el valor del enumerado correspondiente al tipo de elemento que queremos obtener. Así tenemos un método imagen(), fuente(), musica(), sonido() y fuente() que nos permite extrer un elemento de cada tipo de la galería.

Los elementos serán almacenados en elementos aplicación por lo que utilizaremos el tipo map definido en la STL. El elemento clave estará definido sobre los enumerados de la clase y el valor será un puntero al elemento que queremos gestionar.

Veamos la implementación de la clase:

<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"
  4. include "Musica.h"
  5. include "Sonido.h"

using namespace std;


Galeria::Galeria() {

  1. ifdef DEBUG
   cout << "Galeria::Galeria()" << endl;
  1. endif
   // Cargamos las rejillas en la galería para las animaciones
   // y las imágenes fijas
   imagenes[PERSONAJE_PPAL] = new Imagen("Imagenes/personaje_principal.bmp",\

4, 7, 45, 90, false);

   imagenes[TILES] = new Imagen("Imagenes/bloques.bmp", 10, 6);
   imagenes[ENEMIGO_RATA] = new Imagen("Imagenes/enemigo_rata.bmp", 1, 3, 45, 72, false);
   imagenes[ENEMIGO_MOTA] = new Imagen("Imagenes/enemigo_mota.bmp", 1, 3, 45, 72, false);
   imagenes[TITULO_TUTORIAL] = new Imagen("Imagenes/titulo_tutorial.bmp", 1, 1);
   imagenes[TITULO_FIRMA] = new Imagen("Imagenes/titulo_libsdl.bmp", 1, 1);
   imagenes[ITEM_ALICATE] = new Imagen("Imagenes/alicate.bmp", 1, 4, 25, 57);
   imagenes[ITEM_DESTORNILLADOR] = new Imagen("Imagenes/destornillador.bmp", 1, 4, 40, 18);
   // Cargamos las fuentes en la galería
   fuentes[FUENTE_MENU] = new Fuente("Imagenes/arial_black.png", true);
   // Cargamos la música de la galería
   musicas[MUSICA_MENU] = new Musica("Musica/menu.wav");
   musicas[MUSICA_EDITOR] = new Musica("Musica/editor.wav");
   musicas[MUSICA_JUEGO] = new Musica("Musica/juego.wav");
   // Cargamos los sonidos de efectos
   sonidos[COGE_ITEM] = new Sonido("Sonidos/item.wav");
   sonidos[MATA_MALO] = new Sonido("Sonidos/malo.wav");    
   sonidos[PASA_NIVEL] = new Sonido("Sonidos/nivel.wav");  
   sonidos[EFECTO_MENU] = new Sonido("Sonidos/menu.wav");
   sonidos[MUERE_BUENO] = new Sonido("Sonidos/muere.wav");

}


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];

}

Musica *Galeria::musica(codigo_musica cod_music) {

   // Devolvemos la música solicitada
   return musicas[cod_music];

}

Sonido *Galeria::sonido(codigo_sonido cod_sonido) {

   // Devolvemos el sonido solicitado
   return sonidos[cod_sonido];

}


Galeria::~Galeria() {

   // Descargamos la galería
   delete imagenes[PERSONAJE_PPAL];
   delete imagenes[TILES];
   delete imagenes[ENEMIGO_RATA];
   delete imagenes[ENEMIGO_MOTA];
   delete imagenes[TITULO_TUTORIAL];
   delete imagenes[TITULO_FIRMA];
   delete imagenes[ITEM_ALICATE];
   delete imagenes[ITEM_DESTORNILLADOR];
   delete fuentes[FUENTE_MENU];
   delete musicas[MUSICA_MENU];
   delete musicas[MUSICA_JUEGO];
   delete musicas[MUSICA_EDITOR];
   delete sonidos[COGE_ITEM];
   delete sonidos[MATA_MALO];
   delete sonidos[PASA_NIVEL];
   delete sonidos[EFECTO_MENU];
   delete sonidos[MUERE_BUENO];
  1. ifdef DEBUG
   cout << "Galeria::~Galeria()" << endl;
  1. endif

} </cpp>

Como puedes ver la implementación de la clase no es más que un pequeño ejercicio de programación. El constructor inicializa todos los elementos de la galería que serán utilizados en el videojuego. El destructor libera los recursos utilizados por la galería.

Los métodos observadores son muy simples. Acceden a los maps y devuelven el valor consultado que ha sido pasado como parámetros


[editar] La clase Imagen

La clase imagen le da soporte a las rejillas de imágenes que vamos a utilizar en el videojuego. Se encarga de cargar una imagen que esté en cualquier formato compatible con SDL_image e indexar cada uno de los cuadros que la componen. En caso de ser una imagen única tendría un sólo cuadro. Esta forma de actuar es muy práctica a la hora de cargar animaciones ya que nos permite reducir el acceso a disco considerablemente.

Imagínate que para crear una animación hacen falta 20 imágenes. Podemos tener una rejilla con esas 20 imágenes y cargarlas solamente una vez en memoria principal. Con esta clase la tendríamos perfectamente indexada y el hecho de animar la imagen sólo suprondía recorrer las imágenes de la rejilla.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Imagen
Clase Imagen


Vamos a estudiar la definición de la clase:

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

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


class Imagen {

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

int x = 0, int y = 0, bool alpha = false);

   void dibujar(SDL_Surface *superficie, int i, int x, int y, int flip = 1);
   // Consultoras
   int pos_x();
   int pos_y();
   
   int anchura();
   int altura();
   int cuadros();
   
   // Destructor 
   ~Imagen();
private:
   SDL_Surface *imagen;
   SDL_Surface *imagen_invertida;
   // Propiedades de la rejilla de la imagen
   int columnas, filas;
   // Ancho y alto por frame o recuerdo de la animación
   int w, h; 
   // Coordenadas origen
   int x0, y0;
   
   // Rota 180 grado en horizontal
   SDL_Surface * invertir_imagen(SDL_Surface *imagen);

};

  1. endif

</cpp>

La definición de la clase dota de cierta potencia al concepto de imagen. En la parte privada de la clase tenemos la superficie SDL donde almacenaremos la imagen. La superficie donde almacenaremos la invertida de esta imagen. Esta invertida se utiliza para reproducir un movimiento en dos sentidos con una sóla imagen. Tanto de derecha a izquierdas como de izquierdas a derecha.

Dos atributos, columas y filas, nos permiten almacenar el número de filas y columnas de nuestra imagen. El método que utilizamos para invertir la imagen lo estudiaremos con la implementación de la clase que veremos a continuación.

En la parte pública de la clase obsevamos los diferentes métodos que nos ofrece esta clase. Aparte del constructor y el destructor todos los demás métodos son observadores. Podemos obtener el tamaño de un cuadro de la rejilla de imágenes con los métodos anchura() y altura(), el número de cuadros de los que consta la rejilla con caudors(), la posición de una imagen en un determinado momento con pos_x() y pos_y()...

Veamos la implementación de la clase:

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

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

using namespace std;

Imagen::Imagen(char *ruta, int filas, int columnas, int x, int y, bool alpha) {

   // Inicializamos los atributos de la clase
   this->filas = filas;
   this->columnas = columnas;
   x0 = x;
   y0 = y;


  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);

   }
   if(!alpha) {

SDL_Surface *tmp = imagen;

imagen = SDL_DisplayFormat(tmp); SDL_FreeSurface(tmp);

if(imagen == NULL) { printf("Error: %s\n", SDL_GetError()); 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);

   }
   
   // Hallamos la imagen invertida utilizada en el mayor de los casos
   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 de la rejilla 
   // es el total entre el número de columnas	
   w = imagen->w / columnas;
   // El ato de una imagen de la rejilla 
   // 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 - x0;
   destino.y = y - y0;
   // 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:

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

// Realizamos el blit

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

    case -1:

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 posición con respecto a la horizontal de la imagen

int Imagen::pos_x() {

   return x0;

}


// Devuelve la posición con respecto a la vertical de la imagen

int Imagen::pos_y() {

   return y0;

}

// 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 y destino preparados para copiar línea a línea
   origen.x = 0;
   origen.y = 0;
   origen.w = 1;
   origen.h = imagen->h;
   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>

El constructor de la clase inicializa los atributos de dicha clase. Utiliza la librería SDL_image para cargar la imagen que almacenará dicha clase ofreciendo así una mayor potencia que si sólo pudiese cargar el formato nativo de SDL.

Una vez cargada la imagen establecemos el color de la transparencia y la invertimos ya que en nuestra aplicación vamos a necesitar de dichas imágenes invertidas. Un vez realizado todo el proceso calculamos cuanto ocupa un cuadro de la rejilla de imágenes y lo almacenamos en los atributos de la clase.

El método dibujar() nos permite mostrar un recuadro de la rejilla en una posición (x, y) de una superficie pasada como parámetro. El parámetro flip estará establecido a 1 si queremos la imagen original o a -1 si queremos dibujar la invertida. El método comprueba que los parámetros sean correctos y, seguidamente, realiza el blit sobre la superficie destino. Para distinguir si utilizar la imagen original o la invertida utiliza una estructura selectiva. Esta estructura es muy importante ya que la posición de origen de la superficie a copiar varía si la imagen a copiar es la normal o la invertida.

Los demás métodos observadores se limitan a devolver atributos de la clase asociados a la definición del método y no merecen mayor explicación.



[editar] La clase Interfaz

La clase Interfaz es una clase virtual que nos permite definir un interfaz abstracto entre los que movernos según necesite la clase Universo. Esta clase unifica las características de los distintos aspectos del juegos que son el Menú, el Juego y el Editor de niveles.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Interfaz
Clase Interfaz

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Interfaz.h // // Superclase de las diferentes escenas disponibles en la aplicación

  1. ifndef _INTERFAZ_H_
  2. define _INTERFAZ_H_


// Declaración adelantada

class Universo;


class Interfaz {

public:
   
   // Tres son las escenas que encontramos en la aplicación
   enum escenas {

ESCENA_MENU, ESCENA_JUEGO, ESCENA_EDITOR

   };
   // Constructor
   Interfaz(Universo *universo);

   // Funciones virtuales puras comunes a todas las escenas
   virtual void reiniciar(void) = 0;
   virtual void dibujar(void) = 0;
   virtual void actualizar(void) = 0;
   
   Universo *universo;
   
   // Destructor
   virtual ~Interfaz();	

};

  1. endif

</cpp>

Como puedes ver la definción de la clase es bastante breve. Define un tipo enumerado para poder identificar el tipo de escena en el que nos encontramos. Además del constructor y el destructor tenemos tres métodos virtuales que serán implementados en las clases hijas y que dotan de un comportamiento común a las escenas de la aplicación.

Además tenemos un atributo universo} que nos permite asociar esta clase a la clase Universo que integra la aplicación. Veamos la implementación de la clase:

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

  1. include <iostream>
  1. include "Interfaz.h"
  2. include "Universo.h"

using namespace std;


Interfaz::Interfaz(Universo *universo) {

  1. ifdef DEBUG
   cout << "Interfaz::Interfaz()" << endl;
  1. endif
   this->universo = universo;

}


Interfaz::~Interfaz() {

   // No realiza ninguna acción particular

} </cpp>

Como puedes observar sólo está implementado el constructor de la clase que inicializa el único atributo de la clase.


[editar] La clase Item

La clase Item es una subclase de la clase Participante que nos permite definir elementos u objetos del juego con los que nuestro personaje principal puede interactuar. Estos objetos son elementos muy importantes del juego ya que nos permiten ganar puntos o recuperar vidas dotando a la aplicación de toda su esencia.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Item
Clase Item

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Item.h // // Esta clase controla todo lo relativo a los items u objetos del juego

  1. ifndef _ITEM_H_
  2. define _ITEM_H_
  1. include "Participante.h"

class Item: public Participante {

public:
   //Constructor
   Item(enum tipo_participantes tipo, Juego *juego, int x, int y, int flip = 1);
   void actualizar(void);
   void colisiona_con(Participante *otro);
   
   // Destructor
   virtual ~Item();

};

  1. endif

</cpp>

La definición de esta clase responde a la definición de su clase madre. Tenemos el costructor que nos permite crear un ítem de distinto tipo. El método actualizar()} que nos permite avanzar en el estado del objeto en un momento dado. Sólo hay otro método en esta clase. Se trata del método que informa de una colisión con otro participante para poder realizar las acciones oportunas que veremos en la implementación de la clase.

La implementación de esta clase es:

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

  1. include <iostream>
  1. include "Item.h"
  2. include "Juego.h"
  3. include "Universo.h"
  4. include "Galeria.h"
  5. include "Sonido.h"
  6. include "Imagen.h"
  7. include "Control_Animacion.h"
  8. include "Nivel.h"


using namespace std;


Item::Item(enum tipo_participantes tipo, Juego *juego, int x, int y, int flip):

   Participante(juego, x, y, flip) {
  1. ifdef DEBUG
   cout << "Item::Item()" << endl;
  1. endif
   // Animaciones de los estado de los objetos
   animaciones[PARADO] = new Control_Animacion("0,0,0,1,2,2,1", 10);
   animaciones[MORIR] = new Control_Animacion("1,2,3,3", 7);
   // Imagen según el tipo de item creado
   switch(tipo) {
    case TIPO_ALICATE:

imagen = juego->universo->galeria->imagen(Galeria::ITEM_ALICATE); break;

    case TIPO_DESTORNILLADOR:

imagen = juego->universo->galeria->imagen(Galeria::ITEM_DESTORNILLADOR); break;

    default:

cerr << "Item::Item(): Caso no contemplado" << endl; break;

   }
   estado = PARADO;

}


void Item::actualizar(void) {

   // Si el item "muere" lo marcamos para eliminar
   // si no avanzamos la animación
   if(estado == MORIR) {

if(animaciones[estado]->avanzar()) estado = ELIMINAR;

   }
   else 

animaciones[estado]->avanzar();


   // Por si está colocado en las alturas
   // y tiene que caer en alguna superficie
   // necesita una velocidad de caida
   velocidad_salto += 0.1;
   y += altura((int) velocidad_salto);

}


void Item::colisiona_con(Participante *otro) {

   // Si colisiona, muere para desaparecer
   if(estado != MORIR) {

juego->universo->\ galeria->sonidos[Galeria::COGE_ITEM]->reproducir();

estado = MORIR;

   }

}


Item::~Item() {

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

} </cpp>

El constructor de esta clase lo primero que hace es inicializar las animaciones de las que va a hacer uso dicho objeto. Dependiendo del tipo de item cargamos una imagen u otra de la galería para mostrar las animaciones. El estado inicial de los objetos es parado.

En el método actualizar se comprueba si el ítem en cuestión ha "muerto por lo que debe ser eliminado. En cualquier caso se avanza la animación. En el caso de que el objeto no esté sobre un suelo sólido éste caerá hasta encontrarlo.

El método colisiona_con() sirve para informar a un ítem que se ha producido una colisión. En este caso si se produce una colisión marcaremos al objeto con el estado morir así será eliminado en las siguientes iteraciones del game loop.


[editar] La clase Juego

La clase Juego es una clase hija de Interfaz. Esta clase define el comportamiento de la escena Juego que es en la que jugamos propiamente a los niveles que hemos diseñado.

El diseño de la clase del que partimos para la implementación es el de la figura.

Imagen:5Juego.png
Clase CJuego


Clase CJuego
Clase CJuego

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Juego.h // // Esta clase hija de interfaz nos permite relacionar el control del juego // con los niveles para cohesionar los módulos

  1. ifndef _JUEGO_H_
  2. define _JUEGO_H_
  1. include <SDL/SDL.h>
  2. include "Interfaz.h"

// Declaración adelantada

class Universo; class Control_Juego; class Nivel;

class Juego : public Interfaz {

public:
   // Constructor
   Juego(Universo *universo);
   
   // Funciones heradadas
   void reiniciar(void);
   void actualizar(void);
   void dibujar(void);
   
   Control_Juego *control_juego;
   Nivel *nivel;
   // Destructor
   ~Juego();

};

  1. endif

</cpp>

La clase implementa los métodos que hereda de Interfaz y añade dos variables que nos van a permitir asociar dicha clase con las clases Control_Juego y Nivel que son necesarias para el transcurso de una partida. Control_Juego, como su nombre indica, nos provee de control sobre el transcurso del juego y necesitamos la clase que gestiona los niveles para poder interactuar con ellos.

Veamos la implementación de la clase:

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

  1. include <iostream>
  1. include "Participante.h"
  2. include "Juego.h"
  3. include "Universo.h"
  4. include "Nivel.h"
  5. include "Control_Juego.h"
  6. include "Galeria.h"
  7. include "Imagen.h"
  8. include "Musica.h"


using namespace std;


Juego::Juego(Universo *universo): Interfaz(universo) {

  1. ifdef DEBUG
   cout << "Juego::Juego()" << endl;
  1. endif
   nivel = NULL;
   control_juego = NULL;
   // Iniciamos el juego
   reiniciar();

}


void Juego::reiniciar(void) {

   // Hacemos sonar la música
   universo->galeria->musica(Galeria::MUSICA_JUEGO)->pausar();   
   universo->galeria->musica(Galeria::MUSICA_JUEGO)->reproducir();
   // Si se ha iniciado el juego
   // Eliminamos los datos anteriores (restauramos)
   if(control_juego != NULL)

delete control_juego;

   if(nivel != NULL)

delete nivel;

   // Generamos de nuevo el entorno del juego
   control_juego = new Control_Juego(this);
   nivel = new Nivel(universo);
   nivel->generar_actores(control_juego);

  1. ifdef DEBUG
   cout << "Juego::reiniciado" << endl;
  1. endif

}


void Juego::actualizar(void) {

   // Si pulsamos la tecla de salir, salimos al menu
   if(universo->teclado.pulso(Teclado::TECLA_SALIR))

universo->cambiar_interfaz(ESCENA_MENU);

   // Actualizamos la posición de la ventana
   nivel->actualizar();
   // Actualizamos el estado de los items,
   // enemigos y el personaje principal
   control_juego->actualizar();

}


void Juego::dibujar(void) {

   // Dibujamos en la superficie principal
   // El nivel
   nivel->dibujar(universo->pantalla);
   // Los personajes, items y enemigos
   control_juego->dibujar(universo->pantalla);
   // Actualizamos la pantalla
   SDL_Flip(universo->pantalla);

}


Juego::~Juego() {

   // Liberamos la memoria
   delete control_juego;
   delete nivel;
  1. ifdef DEBUG
   cout << "Juego::~Juego()" << endl;
  1. endif

} </cpp>

El constructor de la clase utiliza el constructor de Interfaz así como inicializa los atributos definidos en la clase hija. Una vez construido un elemento de este tipo realiza una llamada al método reiniciar para comenzar el transcurso del juego.

El método reiniciar() comienza a reproducir la música del juego, si existían los elementos nivel o control_juego los elimina así como crea nuevas instancias para estos dos atributos. Una vez realizadas todas estas tareas el método genera los actores y les proporciona el correspondiente control.

El método actualizar() comprueba si se ha pulsado la tecla de salida con la que se volvería al menú principal del juego y llama a los métodos actualizar de nivel y de control_juego para que renueven sus estados.

El método dibujar() se encarga de que nivel y control_juego dibujen mediante sus métodos dibujar() las neuvas posiciones y elementos del nivel. Una vez que hayan realizado el blitting nuestro método dibujar se encarga de hacer el flip de los búfferes con los que mostrar toda esta nueva información.

El destructor de la clase se encarga de liberar la memoria ocupada por los atributos propios de esta clase. Como puedes ver no tiene mayor complicación.

[editar] La clase Menu

Como la clase Juego, esta clase es la encargada de implementar el interfaz de menú en nuestra aplicación. El menú tiene una serie de opciones y animaciones que lo hacen ser un caso concreto de la clase Interfaz de la que es hija. Veamos los aspectos de esta clase.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Menu
Clase Menu


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Menu.h // // Esta clase controla los aspectos relevantes al Menú de la aplicación

  1. ifndef _MENU_H_
  2. define _MENU_H_
  1. include <SDL/SDL.h>
  2. include "Interfaz.h"
  3. include "CommonConstants.h"

// Declaraciones adelantadas

class Teclado; class Control_Movimiento; class Texto; class Imagen;


class Menu: public Interfaz {

public:
   // Constructor
   Menu(Universo *universo);
   // Funciones heradas de Interfaz
   // Operativa de la clase
   void reiniciar (void);
   void actualizar (void);
   void dibujar (void);
   // Destructor
   ~Menu();
   
private:
   // Controla el dispositivo de entrada
   Teclado *teclado;
   // Controla los movimientos y posición de los elementos del menú
   Control_Movimiento *cursor;
   Control_Movimiento *titulo_tutorial;
   Control_Movimiento *titulo_firma;
   // Aquí almacenamos las frases que componen el menú
   Texto *cadena_opciones[NUM_OPCIONES];
   int x, y;
   int opcion;
   Imagen *imagen;
   
   // Crea las cadenas que se almacenarán en cadena_opciones
   void crear_cadenas(void);
   // Crea los títulos del juego
   void crear_titulos(void);

};

  1. endif

</cpp>

En la parte privada de esta clase tenemos varios atributos que nos permiten dotar de comportamiento al menú. El atributo teclado nos permite asociar el dispositivo de entrada con el menú. Los atributos cursor, titulo_tutorial y titulo_firma al ser del tipo Control_Movimiento nos permiten dotar movimiento a estos elementos dentro del menú. Tenemos un atributo cadena_opciones donde almacenamos las cadenas de texto que corresponden con las diferentes alternativas dentro del menú.

Para controlar la posiciones del cursor está los atributos (x, y) mientras que el atributo opcion almacena la elección que hayamos realizado. Mediante el atributo imagen asociaremos la imagen de los elementos del menú. Los métodos privados crear_cadenas() y crear_titulos() los estudiaremos a continuación con la implementación de la clase.

En la parte pública de la clase encontramos los métodos comunes a todas las interfaces: reniciar(), actualizar() y dibujar(). Vamos a presentar la implementación de esta clase para analizar cada uno de estos métodos:

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

  1. include <iostream>
  1. include "Menu.h"
  2. include "Universo.h"
  3. include "Galeria.h"
  4. include "Imagen.h"
  5. include "Sonido.h"
  6. include "Teclado.h"
  7. include "Control_Movimiento.h"
  8. include "Fuente.h"
  9. include "Texto.h"
  10. include "Musica.h"


using namespace std;


Menu::Menu(Universo *universo) : Interfaz(universo) {

  1. ifdef DEBUG
   cout << "Menu::Menu()" << endl;
  1. endif
   // El dispositivo de entrada nos lo da el entorno del juego
   teclado = &(universo->teclado);


   // Cargamos el título del juego
   imagen = universo->galeria->imagen(Galeria::MENU);
   
   x = y = opcion = 0;


   // Cargamos el cursor de selección
   cursor = new Control_Movimiento(universo->galeria->imagen(Galeria::TILES), 50, 100, 300);


   // Creamos los títulos
   crear_titulos();
   // Creo las cadenas de las opciones
   crear_cadenas();
   
   // Inicio el Menu
   reiniciar();

}


void Menu::reiniciar(void) {

   // Hacemos sonar la música
   universo->galeria->musica(Galeria::MUSICA_MENU)->pausar();   
   universo->galeria->musica(Galeria::MUSICA_MENU)->reproducir();
   
   // Colocamos cada parte del título en su lugar
   // Desde una posición de origen hasta la posición final
   titulo_tutorial->mover_inmediatamente(100, -250);
   titulo_tutorial->mover(100,110);
   
   titulo_firma->mover_inmediatamente(640, 200);
   titulo_firma->mover(400, 200);

}


void Menu::actualizar(void) {

   static int delay = 0; // Variable con "memoria"
   // Actualizamos el cursor seleccionador
   cursor->actualizar();
   
   // Actualizamos los títulos
   titulo_tutorial->actualizar();
   titulo_firma->actualizar();
   
   // Si pulsamos abajo y no estamos en la última
   // (controlamos no ir excesivamente rápido)
   if(teclado->pulso(Teclado::TECLA_BAJAR) && opcion < 2 && delay == 0) {

delay = 30; // Retardo de 30 ms opcion++; // Bajamos -> opcion = opcion + 1 cursor->mover(100, 300 + 50 * opcion); // Movemos el cursor

// Reproducimos un efecto de sonido

universo->galeria->sonidos[Galeria::EFECTO_MENU]->reproducir();

   }
   // Si pulsamos arriba y no estamos en la primera opción
   if(teclado->pulso(Teclado::TECLA_SUBIR) && opcion > 0 && delay == 0) {

delay = 30; // Retardo de 30 ms opcion--; // Subimos -> opcion = opcion - 1 cursor->mover(100, 300 + 50 * opcion); // Movemos el cursor

// Reproducimos un efecto de sonido

universo->galeria->sonidos[Galeria::EFECTO_MENU]->reproducir();

   }


   if(delay) // Reducimos el retardo

delay--;


   // Si aceptamos
   if(teclado->pulso(Teclado::TECLA_ACEPTAR)) { 

// Entramos en una opción determinada

switch(opcion) {

case 0: // Jugar

universo->cambiar_interfaz(ESCENA_JUEGO); break;

case 1: // Editar niveles

universo->cambiar_interfaz(ESCENA_EDITOR); break;

case 2: // Salir de la aplicación

universo->terminar(); break; }

   }

}


void Menu::dibujar(void) {

   // Dibujamos un rectángulo de fondo con el color anaranjado
   SDL_FillRect(universo->pantalla, NULL,\

SDL_MapRGB(universo->pantalla->format, 161, 151, 240));

   // Dibujamos el cursor que selecciona una opción u otra
   cursor->dibujar(universo->pantalla);
   // Dibujamos los títulos
   titulo_tutorial->dibujar(universo->pantalla);
   titulo_firma->dibujar(universo->pantalla);
   
   // Dibujamos las 3 cadenas de opciones
   for(int i = 0; i < NUM_OPCIONES; i ++)

cadena_opciones[i]->dibujar(universo->pantalla);

   // Actualizamos la pantalla del entorno
   SDL_Flip(universo->pantalla);

}


Menu::~Menu() {

  1. ifdef DEBUG
   cout << "Menu::~Menu()" << endl;
  1. endif
   // Liberamos memoria
   delete cursor;
   delete titulo_tutorial;
   delete titulo_firma;


   // Tamabién de las cadenas
   for(int i = 0; i < NUM_OPCIONES; i ++)

delete cadena_opciones[i];

}


void Menu::crear_cadenas(void) {

   // Crea las cadenas que mostraremos como opciones
   char textos[][20] = { {"Jugar"}, {"Editar niveles"}, {"Salir"} };


   // Cargamos la fuente
   Fuente *fuente = universo->galeria->fuente(Galeria::FUENTE_MENU);
   
   // La amacenamos en el vector de tipo Texto
   for(int i = 0; i < NUM_OPCIONES; i ++) {

cadena_opciones[i] = new Texto(fuente, 150, 300 + i * 50, textos[i]);

   }

}


void Menu::crear_titulos(void) {

   // Creamos los títulos animados
   titulo_tutorial = 

new Control_Movimiento(universo->galeria->imagen(Galeria::TITULO_TUTORIAL),\ 0, 300, 200);

   titulo_firma =

new Control_Movimiento(universo->galeria->imagen(Galeria::TITULO_FIRMA),\ 0, 300, 400); } </cpp>

El constructor de la clase inicializa los atributos de la misma creando un control de movimiento para el cursor, creando los títulos, las cadenas que los componen así como reiniciando el estado para que todos los elementos que componen el menú estén en un estado inicial.

El método reiniciar() se encarga de mostrar las animaciones iniciales del menú así como de reestablecer la música correspondiente a esta escena del juego. Los títulos del juego se mueven hacia la posición final desde una posición exterior para producir un efecto de movimiento

En el método actualizar() renovamos el estado del cursor y de los dos títulos que componen la escena. Comprobamos si se ha pulsado alguna de las teclas que producen movimiento entre las opciones del menú. Si es así marcamos un retardo y mostramos un efecto de movimiento así como reproducimos un sonido asociado a dicho evento. Si se pulsa la tecla definida como ACEPTAR cambiamos de escena a la que tuviésemos seleccionada como opción.

El método dibujar() rellenamos el fondo de un color liso para seguidamente "pintar encima de él todos los elementos del menú. Empezamos por el cursor, seguidamente mostrámos los títulos y para terminar hacemos blit sobre la superficie principal con las cadenas que nos muestran las opciones. Para mostrar todos estos cambios hacemos una llamada a la función SDL_Flip() alternando los búfferes.

El destructor libera la memoria utilizada por los atributos de esta clase. Ahora vamos a describir los métodos privados de la clase. El primero de ellos es crear_cadenas(). Con este método creamos las cadenas que luego serán las opciones del menú con la fuente inicializada en el mismo método. Los textos de estas cadenas son introducidos en un vector que será recorrido a la hora de mostrar dichas opciones.

El método privado crear_titulos() crea los elementos decorativos del menú, que como bien indica el nombre del método, es el título de la aplicación que estamos desarrollando. Además al ser del tipo Control_Movimiento los dotamos del movimiento que ya vimos en el método actualizar().


[editar] La clase Musica

La clase Musica nos permite dotar de este elemento a nuestro videojuego. Hemos decidido no complicar la definición de esta clase porque no es necesario para que nos ofrezca toda la potencia que necesitamos.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Musica
Clase Musica

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Musica.h // // Clase para facilitar el trabajo con la musica

  1. ifndef _MUSICA_H_
  2. define _MUSICA_H_
  1. include <SDL/SDL.h>
  2. include <SDL/SDL_mixer.h>

class Musica {

public:
   // Constructor
   Musica(char *ruta);
   void reproducir();
   void pausar();
   ~Musica();
private:
   Mix_Music *bso;

};

  1. endif

</cpp>

Para implementar el control de la música vamos a utilizar la librería auxiliar SDL_mixer. De ahí que en la parte privada de la clase tengamos un atributo bso de un tipo de esta librería que es el que asociará la música con la clase.

En cuanto a la parte pública podemos ver que, además del constructor y el destructor, tenemos dos métodos que nos permitirán reproducir y detener la música. Vamos a ver la implementación de la clase.

<cpp> // Listado: Musica.cpp // // Implementación de la clase Música

  1. include <iostream>
  1. include "Musica.h"
  2. include "CommonConstants.h"

using namespace std;

Musica::Musica(char *ruta) {


   // Cargamos la música
   bso = Mix_LoadMUS(ruta);
   if(bso == NULL) {

cerr << "Música no disponible" << endl; exit(1);

   }
   // Establecemos un volumen predeterminado
   Mix_VolumeMusic(VOLUMEN_MUSICA);
  1. ifdef DEBUG
   cout << "Música cargada" << endl;
  1. endif

}

void Musica::reproducir() {

  Mix_PlayMusic(bso, -1);

}


void Musica::pausar() {

   Mix_PauseMusic();

}

Musica::~Musica() {

   Mix_FreeMusic(bso);

} </cpp>

El constructor de la clase carga el sonido en el atributo destinado para ello y establece un volumen predeterminado para la música. El método reproducir() utiliza una función de la librería auxiliar para hacer sonar la música del juego y el método pausar() hace una llamada a Mix_PauseMusic() con el fin de detener la música.

El destructor libera los recursos asociados al atributo de la clase mediante la función Mix_FreeMusic().


[editar] La clase Nivel

La clase nivel es una de las más importantes del desarrollo de nuestra aplicación. Esta clas ese encarga de controlar todo lo referente a la construcción, carga y almacenado de los niveles así como de los elementos que componen dicho nivel.

Clase Nivel
Clase Nivel


El diseño de la clase del que partimos para la implementación es el de la figura CNivel}.

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Nivel.h // // Esta clase controla todo lo relevante a la construcción // de los nivel del juego

  1. ifndef _NIVEL_H_
  2. define _NIVEL_H_
  1. include <SDL/SDL.h>
  2. include "Ventana.h"
  3. include "CommonConstants.h"

class Ventana; class Imagen; class Universo; class Control_Juego;

class Nivel {

public:
   
   // Constructor (al menos el tamaño de una ventana)
   Nivel(Universo *universo,\

int filas = FILAS_VENTANA, int columnas = COLUMNAS_VENTANA);


   // Funciones de dibujo
   void dibujar(SDL_Surface *superficie);
   void dibujar_actores(SDL_Surface *superficie);
   // Actualizando el nivel
   void actualizar(void);
   int altura(int x, int y, int rango);
   // Comprueba si el elemento es traspasable
   bool no_es_traspasable(char codigo);
   
   // Pasa al nivel siguiente o al anterior
   void siguiente(void);
   void anterior(void);
   // Funciones para la edición del nivel
   void editar_bloque(int i, int x, int y);
   void guardar(void);
   int cargar(void);
   void limpiar(void);
   // Devuelve el número de nivel
   int indice(void);
   
   // Genera los actores que participan en el nivel
   void generar_actores(Control_Juego *procesos);
   
   // Foco de la acción
   Ventana *ventana;
   // Destructor
   ~Nivel();
   
private:
   int numero_nivel;
   int filas, columnas;
   // indica si este nivel ha sido editado
   bool modificado;
   // Almacena las características del nivel
   char mapa[FILAS_NIVEL][COLUMNAS_NIVEL];
   // Para guardar el nivel
   FILE *fichero;
   
   Imagen *bloques;
   Universo *universo;
   
   // Funciones para el manejo de ficheros
   void abrir_fichero(void);
   void cerrar_fichero(void);
   FILE *crear_fichero(void);
   void copiar_fichero(FILE *tmp);

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varios atributos y un varios métodos para la gestión de ficheros. El atributo numero_nivel almacena en qué nivel nos estamos moviendo actualmente, ya sea para editarlo o durante la partida. Los atributos filas y columnas almacenan el número de filas y columnas en tiles o recuadros que componen el nivel.

Utilizamos un atributo booleano modificado para saber si existen cambios en el nivel. Esto es útil cuando estamos editando el nivel para guardarlo sólo en el caso de que existan cambios o bien, si se quiere salir del editor y no se han guardado los cambios, para avisar al usuario de este incidente.

El siguiente atributo mapa es el que tiene toda la información del nivel. Es del tipo matriz de char ya que es suficiente con 8 bits para detallar la información de cada tile. En cada posicón de la matriz indicaremos la información que debe almacenarse/mostrarse del nivel.

El atributo fichero asocia un fichero de niveles a la clase mientras que los métodos abrir_fichero(), cerrar_fichero, crear_fichero() y copiar_fichero() nos permiten trabajar con este fichero cómodamente.

Los atributos bloques y universo asocian un elemento de la clase Imagen y al nivel con el Universo que lo engloba.

En la parte pública de la clase tenemos todos métodos necesarios para interactuar con los niveles. El método dibujar() se encarga de mostrar la escena del nivel por pantalla mientras que el método dibujar_actores() hace lo propio con los participantes del nivel.

El siguiente método que definimos es un método común a muchas de nuestras clases. Se trata de actualizar() y la única tarea que realiza es la de posicionar la ventana que se nos muestra del nivel en el lugar correcto. El método altura() calcula la diferencia entre la posición del participante y el elemento no traspasable más próximo en el eje y. Esta función toma como parámetro un rango como comprobación máxima de altura que devolverá en caso de que no exista elemento de referencia para calcular dicha altura.

El método no_es_traspasable() nos permite conocer si un objeto del mapa se puede traspasar o no. La utilidad reside en poder comprobar si elementos que, por ejemplo, pueden ser decorativos vamos a usarlos como plataformas para componer el nivel.

Los métodos siguiente y anterior nos permiten navegar por los distintos niveles que tenemos almacenados en el fichero de niveles. Los siguientes métodos son útiles para gestionar las operaciones que ponemos realizar con el editor de niveles. editar_bloque() nos permite modificar el bloque de la posición (x, y) con el elemento i de la rejilla de utilidades. El método guardar() lo utilizamos para almacenar el nivel en el fichero de niveles. Como podrás intuir el método cargar() nos permite traer el nivel a memoria principal ya sea para editarlo o para jugar sobre él. El método limpiar() deja el nivel actual en blanco sin ningún elemento para que podamos comenzar a construir nuestra aplicación.

El método indice() devuelve el número de nivel actual en el que nos encontramos y generar_actores() crea todos los participantes necesarios sobre el nivel sobre el que vamos a jugar para que podamos interactuar con ellos.

El atributo ventana asocia una clase de este tipo con la clase Nivel para poder movernos por el mismo durante el juego o la edición del nivel. Vamos a estudiar la definición de la clase:


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

  1. include <iostream>
  1. include "Nivel.h"
  2. include "Control_Juego.h"
  3. include "Universo.h"
  4. include "Protagonista.h"
  5. include "Juego.h"
  6. include "Imagen.h"
  7. include "Galeria.h"

using namespace std;


Nivel::Nivel(Universo *universo, int filas, int columnas) {

  1. ifdef DEBUG
   cout << "Nivel::Nivel()" << endl;
  1. endif
   // Inicializamos las variables
   
   this->universo = universo;
   this->bloques = universo->galeria->imagen(Galeria::TILES);
   this->filas = filas;
   this->columnas = columnas;
   
   numero_nivel = 0;
   fichero = NULL;
   modificado = false;
   ventana = new Ventana(filas, columnas);
   
   // Cargamos el fichero
   abrir_fichero();
   cargar();
   

}

// Dibuja los bloques del nivel

void Nivel::dibujar(SDL_Surface *superficie) {

   // Columna y fila que se lee del mapa
   int lx, ly; 
   // Zona que se pierde al dibujar el primer bloque si no es múltiplo de 32
   int margen_x, margen_y;
   // Número de bloques a dibujar sobre x e y
   int num_bloques_x, num_bloques_y;
   // 1 bloque - 8 bits
   char bloque;
   // Posicionamos 
   ly = ventana->pos_y() / TAMANO_BLOQUE;
   lx = ventana->pos_x() / TAMANO_BLOQUE;
   // Cálculo del sobrante
   margen_y = ventana->pos_y() % TAMANO_BLOQUE;
   margen_x = ventana->pos_x() % TAMANO_BLOQUE;


   // Si hay sobrante necesitamos un bloque más
   if(margen_x == 0)

num_bloques_x = columnas;

   else 

num_bloques_x = columnas + 1;

   if(margen_y == 0)

num_bloques_y = filas;

   else

num_bloques_y = filas + 1;


   // Dibujamos los bloques
   for(int col = 0; col < num_bloques_x; col++) {

for(int fil = 0; fil < num_bloques_y; fil++) {

bloque = mapa[fil + ly][col + lx];

if(bloque != -1 && bloque < 36) { bloques->dibujar(superficie, bloque,\ col * TAMANO_BLOQUE - margen_x,\ fil * TAMANO_BLOQUE - margen_y, 1); } }

   }

}


void Nivel::dibujar_actores(SDL_Surface *superficie) {

   // Columna y fila que se lee del mapa
   int lx, ly; 
   // Zona que se pierde al dibujar
   // el primer bloque si no es múltiplo de 32
   int margen_x, margen_y;
   // Número de bloques a dibujar sobre x e y
   int num_bloques_x, num_bloques_y;
   // 1 bloque - 8 bits
   char bloque;


   // Posición según bloque
   ly = ventana->pos_y() / TAMANO_BLOQUE;
   lx = ventana->pos_x() / TAMANO_BLOQUE;
   
   // Calculamos el sobrante
   margen_y = ventana->pos_y() % TAMANO_BLOQUE;
   margen_x = ventana->pos_x() % TAMANO_BLOQUE;
   
   
   // Si hay sobrante necesitamos un bloque más
   if(margen_x == 0)

num_bloques_x = columnas;

   else 

num_bloques_x = columnas + 1;

   if(margen_y == 0)

num_bloques_y = filas;

   else

num_bloques_y = filas + 1;


   Imagen *imagen_tmp;
   Galeria::codigo_imagen codigo_tmp;
   int x0, y0;


   for(int col = 0; col < num_bloques_x; col++) {

for(int fil = 0; fil < num_bloques_y; fil++) {

bloque = mapa[fil + ly][col + lx];

if(bloque > 35 && bloque < 45) {

switch(bloque) {

case 36:

x0 = 16; y0 = TAMANO_BLOQUE; codigo_tmp = Galeria::ENEMIGO_RATA; break;

case 37:

x0 = 16; y0 = TAMANO_BLOQUE; codigo_tmp = Galeria::ENEMIGO_MOTA; break;

case 42:

x0 = 16; y0 = TAMANO_BLOQUE; codigo_tmp = Galeria::ITEM_ALICATE; break;

case 43:

x0 = 16; y0 = TAMANO_BLOQUE; codigo_tmp = Galeria::ITEM_DESTORNILLADOR; break;


case 44:

x0 = 16; y0 = TAMANO_BLOQUE; codigo_tmp = Galeria::PERSONAJE_PPAL; break;

default:

codigo_tmp = Galeria::TILES; // que componen la escena break; }

if(codigo_tmp != Galeria::TILES) {

imagen_tmp = universo->galeria->imagen(codigo_tmp); imagen_tmp->dibujar(superficie, 0,\ col * TAMANO_BLOQUE - margen_x + x0,\ fil * TAMANO_BLOQUE - margen_y + y0, 1); } } }

   }
   

}


// Actualiza la ventana en la que nos encontramos

void Nivel::actualizar(void) {

ventana->actualizar(); }


// Calcula la altura de un elemento

int Nivel::altura(int x, int y, int rango) {

   // Indicamos si estamos fuera de la ventana
   if(x < 0 || x >= ANCHO_VENTANA * 2 || y < 0 || y >= ALTO_VENTANA * 2)

return rango;

   int fila, columna;



   for(int h = 0; h < rango; h++) {

columna = x / TAMANO_BLOQUE; fila = (y + h) / TAMANO_BLOQUE;

if((y + h) % TAMANO_BLOQUE == 0 && no_es_traspasable(mapa[fila][columna])) return h;

   }
   return rango;

}


// Devuelve true si un elemento no es traspasable

bool Nivel::no_es_traspasable(char codigo) {

   // Codigo de la rejilla de los bloques
   // En la imagen que almacena los tiles
   // estos elementos son los que están definidos 
   // como no transpasables
   if(codigo >= 0 && codigo <= 5 || codigo >= 30 && codigo <= 35)

return true;

   else

return false;

}


// Esta función edita el bloque actual // Si se le pasa -1 en i, limpia el bloque // Las posiciones x e y son relativas

void Nivel::editar_bloque(int i, int x, int y) {

   // Calculamos la posición absoluta
   int fila_destino = (y + ventana->pos_y()) / TAMANO_BLOQUE;
   int columna_destino = (x + ventana->pos_x()) / TAMANO_BLOQUE;
   
   // Marcamos el bloque
   mapa[fila_destino][columna_destino] = i;
   // Si se edita un bloque se activa
   // la opción de guardar el nivel modificado
   modificado = true;

}


// Esta función almacena el mapa en un fichero

void Nivel::guardar(void) {

   FILE * salida;
   if(modificado == false) {

cerr << "Nivel::guardar() ->" << " Nivel no modificado, no se almacenará" << endl; return;

   }
   salida = fopen("niveles.dat", "rb+");
   // Si no existe, intentamos crear un fichero
   if(salida == NULL) {

salida = crear_fichero();

if(salida == NULL) {

// No podemos crearlo

cerr << "Nivel::guardar() -> Sin acceso de " << "escritura al sistema de ficheros" << endl; return; }

   }


   if(fseek(salida, BLOQUES_NIVEL * numero_nivel, SEEK_SET)) {

cerr << "Nivel::guardar() -> Error en el fichero" << endl; fclose(salida); return;

   }
   else {

if(fwrite(&mapa, sizeof(char), BLOQUES_NIVEL, salida) < BLOQUES_NIVEL) {

cerr << "Nivel::guardar() -> Error de " << "escritura en el fichero" << endl;

fclose(salida); return; }

   }
   modificado = false; // Una vez guardado, ya no está modificado
   fclose(fichero);
   
   fflush(salida);
   fichero = salida;

}


// Carga el fichero de niveles

int Nivel::cargar(void) {

   if(fseek(fichero, BLOQUES_NIVEL * numero_nivel, SEEK_SET)) {

cerr << "Sin acceso al fichero de niveles" << endl; return 1;

   }
   else {

if(fread(&mapa, sizeof(char), BLOQUES_NIVEL, fichero) < BLOQUES_NIVEL) { cerr << "No se puede cargar el fichero de niveles" << endl; return 1;

}

   }
   
   return 0;

}


// Limpia el mapa del nivel

void Nivel::limpiar(void) {

   // Rellena a -1 todas las posiciones
   memset(mapa, -1, BLOQUES_NIVEL);

}


// Pasa de nivel

void Nivel::siguiente(void) {

   // Un nivel más
   numero_nivel++;
   
  
   if(cargar())

// Si no podemos acceder a un nivel más (no existe, o fallo) numero_nivel--; }


// Pasa al nivel anterior

void Nivel::anterior(void) {

   // Nivel inicial es el 0
   if(numero_nivel > 0) {

numero_nivel--; cargar(); // el nivel

   }

}


int Nivel::indice(void) {

   return numero_nivel;

}


void Nivel::generar_actores(Control_Juego * control_juego) {

   int bloque;
   int x, y;
   for(int col = 0; col < COLUMNAS_NIVEL; col ++) {

for(int fil = 0; fil < FILAS_NIVEL; fil ++) {

// Según la información del bloque

bloque = mapa[fil][col];

x = (col * TAMANO_BLOQUE) + 16; y = (fil * TAMANO_BLOQUE) + TAMANO_BLOQUE;

// Cargamos el actor correspodiente

switch(bloque) {

case 36:

control_juego->enemigo(Participante::TIPO_ENEMIGO_RATA,\ x, y, 1); break;

case 37: control_juego->enemigo(Participante::TIPO_ENEMIGO_MOTA,\ x, y, 1); break;

case 42: control_juego->item(Participante::TIPO_ALICATE,\ x, y, 1); break;

case 43: control_juego->item(Participante::TIPO_DESTORNILLADOR,\ x, y, 1); break;

case 44: control_juego->protagonista(x, y, 1); break;

default: break;

} }

   }

}


Nivel::~Nivel(void) {

  1. ifdef DEBUG
   cout << "Nivel::~Nivel()" << endl;
  1. endif
   delete ventana;
   cerrar_fichero();

}


void Nivel::abrir_fichero(void) {

   fichero = fopen("niveles.dat", "rb");
   if(fichero == NULL)

cerr << "No se encuentra el fichero niveles.dat" << endl;

}


void Nivel::cerrar_fichero(void) {

   // Si existe el fichero
   if(fichero) {

fclose(fichero); // Lo cerramos fichero = NULL;

   }
   else {

cerr << "El fichero de niveles no estaba abierto" << endl;

   }

}


FILE * Nivel::crear_fichero(void) {

   FILE * tmp;
   
   tmp = fopen("niveles.dat", "wb+");
   
   if(tmp == NULL)

cerr << "No se puede crear el fichero niveles.dat" << endl;

   else {

// Lo copiamos en el nuestro

copiar_fichero(tmp); cout << "Fichero de niveles creado" << endl;

   }
   
   return tmp;

}


// Copiamos el fichero abierto a nuestro fichero de niveles // para facilitar la edición

void Nivel::copiar_fichero(FILE * tmp) {

   char mapa_tmp[FILAS_NIVEL][COLUMNAS_NIVEL];
   for(int i = 0; i < COLUMNAS_NIVEL; i ++) {

// Copiamos el fichero, nivel a nivel

fseek(fichero, i * BLOQUES_NIVEL, SEEK_SET); fseek(tmp, i * BLOQUES_NIVEL, SEEK_SET);

fread(mapa_tmp, BLOQUES_NIVEL, 1, fichero); fwrite(mapa_tmp, BLOQUES_NIVEL, 1, tmp);

   }
   cout << "Almacenado fichero de niveles" << endl;

} </cpp>

Vamos a empezar analizando el constructor de la clase. El constructor inicializa todos los atributos de la clase y crea una nueva ventana que nos permita navegar por el nivel. Una vez realizada esta tarea abre el fichero de niveles y carga el primer nivel en el editor o en la propia escena de juego.

El método dibujar() realiza un cálculo de los bloques que tiene que dibujar en la ventana teniendo en cuenta que es posible que haya bloques que no tengan que dibujarse enterso. Una vez calculado se recorre el contenido de la variable mapa que contiene la información de los elemenetos del nivel dibujando en cada bloque el elemento que corresponda.

El método dibujar_actores() es análogo al anterior pero en vez de dibujar los elementos correspondientes en cada uno de los tiles lo que hace es convertir los tiles marcados con el código de algún personaje en el propio personaje. Así conseguimos generar todos los actores reales del videojuego.

La implementación del método altura() comprueba que el elemento esté dentro del nivel. Si es así calcula su altura dentro de un rango máximo. Lo que hace es comprobar los bloques que existen desde la posición en el eje y del elemento a comprobar hasta que se acaba el nivel de los elementos hasta comprobar si existe alguno que no sea traspasable para así identificar la altura.

El método no_es_traspasable() devuelve un valor verdadero si el elemento que recibe como parámetro no es traspasable, como no podía ser de otra forma. Utiliza una estructura selectiva para comprobar el código del elemento. Hemos definido los primeros 6 y los últimos 6 elementos de la rejilla de tiles como elementos no traspasables o sólidos.

La edición de un bloque del nivel es bastante sencilla. Se calcula sobre que bloque está situado el cursor del ratón y seguidamente establecemos en dicha posición el código i del elemento que queremos colocar en dicho lugar. Para terminar marcamos el nivel a modificado para que sea tenido en cuanta a la hora de grabar el nivel.

Para guardar el nivel hacemos uso del método guardar(). Este método nos permite guardar el nivel en la posición correspondiente al número de nivel que queramos guardar. La primera comprobación que realiza es que el nivel haya sido modificado para luego abrir el fichero de datos, buscar la posición donde debemos guardarlo almacenando el contenido de la variable mapa en dicho fichero. Una vez realizada esta tarea se marca la bandera modificado a false ya que no existen modificaciones sin guardar y se reestablece el fichero.

El métdodo cargar() simplemente busca la posición del fichero donde está almanacenado y carga la información del nivel en la variable mapa. El método limpiar rellena el contenido de mapa con valores -1 que indican que la posición de bloque está vacía. Los métodos siguiente() y anterior() cargan los respectivos niveles en la variable mapa en caso de no existir vuelven al nivel de partida.

El método generar_actores() establece en cada bloque marcado con un código referente a un participante un control sobre dicho personaje. Esto dotará a cada imagen de cierto comportamiento que dotará al juego de interactividad.

El resto de los métodos de la clase son simples ejercicios de programación que nos permiten manejar ficheros de disco. Utilizamos funciones C para este manejo para que te sea más familiar aunque el manejo de fichero en C++ de ficheros es muy potente y cómodo de utilizar.


[editar] La clase Participante

La clase participante es la clase madre de todos los actores que convergen en los niveles del videojuego. Nos permite establecer unas características generales para todos estos participantes y poderlos manejar así de una manera más o menos común.

El diseño de la clase del que partimos para la implementación es el de la figura CParticipante}.

Clase Participante
Clase Participante


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Participante.h // // Clase madre que proporciona una interfaz para los participantes // que componen el juego

  1. ifndef _PARTICIPANTE_H_
  2. define _PARTICIPANTE_H_
  1. include <iostream>
  2. include <map>
  3. include <SDL/SDL.h>

class Control_Animacion; class Imagen; class Juego;

using namespace std;

class Participante {

public:
   // Tipos de participantes
   enum tipo_participantes {

TIPO_PROTAGONISTA, TIPO_ENEMIGO_RATA, TIPO_ENEMIGO_MOTA, TIPO_ALICATE, TIPO_DESTORNILLADOR

   };
   // Estados posibles de los participantes
   enum estados {

PARADO, CAMINAR, SALTAR, MORIR, ELIMINAR, GOLPEAR

   };
   
   // Constructor
   Participante(Juego *juego, int x, int y, int direccion = 1);
 
   // Consultoras 
   int pos_x(void);
   int pos_y(void);


   virtual void actualizar(void) = 0;
   void dibujar(SDL_Surface *pantalla);
   // 
   virtual void colisiona_con(Participante *otro) = 0;
   virtual ~Participante();
   
   estados estado_actual(void) {return estado;};
   
protected:
   void mover_sobre_x(int incremento);
   bool pisa_el_suelo(void);
   bool pisa_el_suelo(int _x, int _y);
   int altura(int rango);
   
   Imagen *imagen;
   Juego *juego;
   int x, y;
   int direccion;
   float velocidad_salto;
   
   enum estados estado;
   enum estados estado_anterior;
   
   map<estados, Control_Animacion*> animaciones;

};

  1. endif

</cpp>

En la parte protegida de la clase tenemos varios atributos y métodos que al ser heredados se convertirán en la parte priva de los items, del personaje principal y de sus adversarios. Entre estos atributos tenemos dos que asocian esta clase con las clases Imagen y Juego. Cada personaje tiene una imagen o rejilla que lo representa gráficamente dentro de un juego de ahí estas variables.

El atributo direccion indica si el movimiento del personaje es hacia la derecha con un 1 y hacia la izquierda con un -1. Los atributos (x, y) sirven para establecer la posición de cualqueir participante y la velicidad_salto se aplica cuando dicho participante no está sobre un elemento no traspasable. Además de todo esto el método define una variable estado para saber la situación en la que se encuentra y un estado_anterior para saber como llegamos a dicho estado.

Para terminar con los atributos de parte protegida la clase dispone de un map donde almacenaremos todas las animaciones asociadas a dicho elemento. Al utilizar esta implementación de la aplicación tendremos un acceso directo a dichas animaciones además de estar gestionadas de formrma dinámica. En versiones anteriores utilizamos un vector con el consecuente desperdicio de memoria.

En la parte protegida también tenemos varios métodos auxiliares. El método mover_sobre_x() mueve y controla el moviemiento sobre el eje horizontal mientras que pisa_el_suelo() tiene dos versiones para comprobar si un elemento está sobre una superficie no traspasable. La función altura() calcula la altura de la posición del participante dentro de un rango análogamente a como se hacía en la clase Nivel.

En la parte pública de la clase definimos dos enumerados que nos van a permitir definir los estados que pueden tomar los participantes así como el tipo de participantes que van a coexistir en el videojuego. La idea es utilizar estos enumerados con el fin de que la indexación de la aplicación donde almacenamos las animaciones de cada personaje sea más cómoda.

Los métodos que ofrece la parte pública ya son conocidos por nosotros. Se tratan de, aparte del constructor y el destructor, los métodos que nos permiten actualizar la lógica de los participantes (actualizar()) y mostrar los participantes en la pantalla (dibujar()). Además de estos incorpora un método virtual colisiona_con() que nos permite informar a un elemento del juego que ha colisionado.

Como métodos consultores podemos obtener la posición del participante así como el estado actual del mismo mediante los métodos estado_actual(), pos_x() y pos_y(). Vamos a estudiar la implementación de esta clase.

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


  1. include <iostream>
  1. include "Participante.h"
  2. include "Juego.h"
  3. include "Nivel.h"
  4. include "Control_Animacion.h"
  5. include "Imagen.h"

using namespace std;


Participante::Participante(Juego *juego, int x, int y, int direccion) {

  1. ifdef DEBUG
   cout << "Participante::Participante()" << endl;
  1. endif
   // Inicializamos las variables
   this->juego = juego;
   this->direccion = 1;
   this->x = x;
   this->y = y;
   velocidad_salto = 0.0;

}


int Participante::pos_x(void) {

   return x;

};


int Participante::pos_y(void) {

   return y;

};


Participante::~Participante() {

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

}

void Participante::mover_sobre_x(int incremento) {

   if(incremento > 0) {

if(x < ANCHO_VENTANA * 2 - 30) x += incremento;

   }
   else {

if(x > 30) x += incremento;

   }

}


int Participante::altura(int rango) {

return juego->nivel->altura(x, y, rango); }


bool Participante::pisa_el_suelo(void) {

if(altura(1) == 0) return true; else return false; }


bool Participante::pisa_el_suelo(int _x, int _y) {

   if(juego->nivel->altura(_x, _y, 1) == 0)

return true;

   else

return false;

}


void Participante::dibujar(SDL_Surface *pantalla) {

   imagen->dibujar(pantalla, animaciones[estado]->cuadro(),\

x - juego->nivel->ventana->pos_x(),\ y - juego->nivel->ventana->pos_y(), direccion); } </cpp>

Como podía suponer la implementación de esta clase es muy ligera ya que la esencia de los métodos estará implementada en sus clases hijas. El constructor de la clase se limita a inicializar los valores de los atributos de la misma y de los demás métodos públicos sólo se implementan las funciones que devuelven la posición del participante en un momento dado ya que es una codificación común para todos los tipos de participantes del juego.

El método mover_sobre_x() controla el movimiento sobre el eje horizontal y el rango que tiene el mismo mientras que la implementación del método altura() utiliza el el método que presentamos en la clase Nivel para el cálculo de la misma.

Para comprobar si un participante pisa_el_suelo() se comprueba si en un rango de un píxel se puede calcular la altura. Si es así se estaría pisando el suelo.

El método dibujar() utiliza el que implementamos en la clase Imagen para mostrar una imagen en una superficie en una posición determinada utilizando esta vez el cuadro actual de la animación del participante del estado actual en el que se encuentre.


[editar] La clase Protagonista

La clase Protagonista es una especificación de la clase Participante por lo que es similar a esta última.

El diseño de la clase del que partimos para la implementación es el de la figura CProtagonista.


Clase Protagonista
Clase Protagonista

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Protagonista.h // // Esta clase controla los distintos aspectos del proyagonista


  1. ifndef _PROTAGONISTA_H_
  2. define _PROTAGONISTA_H_
  1. include <SDL/SDL.h>
  2. include "Participante.h"

// Declaración adelantada

class Juego; class Participante; class Teclado;

class Protagonista: public Participante {

public:
   
   // Constructor
   Protagonista(Juego *juego, int x, int y, int direccion = 1);
   
   void actualizar(void);
   void colisiona_con(Participante *otro);
   
   // Destructor
   ~Protagonista();
private:
   
   // Dispositivo que controla el personaje
   Teclado *teclado;
   
   int x0, y0;
   
   void reiniciar(void);
   
   // Estados del protagonista
   // Para la implementación del autómata
   void estado_caminar(void);
   void estado_parado(void);
   void estado_disparar(void);
   void estado_saltar(void);
   void estado_morir(void);
   void estado_comenzar_salto(void);

};

  1. endif

</cpp>

Añade a los métodos de su clase madre varios métodos privados auxiliares para implementar el autómata que hemos diseñado para conocer que estados y movimientos puede realizar el protagonista en un momento determinado.

Además incluye un atributo que permite asociar al personaje con el teclado que será el dispositivo de entrada. Las acciones sobre el teclado determinaran los cambios en el diagrama de transiciones o de estados. Cuando muera el protagonista (y si todavía le quedasen vidas) debe de reiniciar su estado y el de la animación que lo representa al estado inicial del juego. Para realizar esta tarea se ha incluido el método reiniciar().

Existen dos atributos nuevos que nos permiten almacenar la posición original del protagonista por si tenemos que restaurlo a ella después de que ocurre alguna acción del juego.

Pasemos a estudiar la implementación de la clase:

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

  1. include <iostream>
  1. include "Protagonista.h"
  2. include "Juego.h"
  3. include "Teclado.h"
  4. include "Galeria.h"
  5. include "Sonido.h"
  6. include "Universo.h"
  7. include "Nivel.h"
  8. include "Imagen.h"
  9. include "Control_Animacion.h"


using namespace std;


Protagonista::Protagonista(Juego *juego, int x, int y, int direccion):

   Participante(juego, x, y, direccion)

{

  1. ifdef DEBUG
   cout << "Protagonista::Protagonista()" << endl;
  1. endif
   // Inicializamos los atributos de la clase
   this->juego = juego;
   this->teclado = &(juego->universo->teclado);
   imagen = juego->universo->galeria->imagen(Galeria::PERSONAJE_PPAL);
   // Asociamos al personaje las animaciones
   // según la rejilla que cargamos para controlarlo
   animaciones[PARADO] = new Control_Animacion("0", 5);
   animaciones[CAMINAR] = new Control_Animacion("1,2,3,2,1,0,4,5,6,5,4,0", 6);
   animaciones[SALTAR] = new Control_Animacion("21,19", 0);
   animaciones[GOLPEAR] = new Control_Animacion("15", 20);
   animaciones[MORIR] = new Control_Animacion("22, 23, 23, 24, 25, 23, 25", 10);
   
   x0 = x;
   y0 = y;
   
   reiniciar();

}


void Protagonista::actualizar(void) {

   // Establecemos la posición de l a ventana
   juego->nivel->ventana->establecer_pos(x - 320, y - 240);
   // Si no estamos en el mismo estado 
   if(estado != estado_anterior) {

// Comenzamos la animación y guardamos el estado

animaciones[estado]->reiniciar(); estado_anterior = estado;

   }
   // Implementación del autómata
   // Según sea el estado
   switch(estado) {
    case PARADO:

estado_parado(); break;

    case CAMINAR:

estado_caminar(); break;

    case GOLPEAR:

estado_disparar(); break;

    case SALTAR:

estado_saltar(); break;

    case MORIR:

estado_morir(); break;

    default:

cout << "Estado no contemplado" << endl; break;

   }
   

}

void Protagonista::colisiona_con(Participante *otro) {

   if(estado != MORIR) {

juego->universo->\ galeria->sonidos[Galeria::MUERE_BUENO]->reproducir();

estado = MORIR; // Muere el personaje velocidad_salto = -5; // Hace el efecto de morir

   }

}


void Protagonista::reiniciar(void) {

   // Reestablecemos el personaje
   
   x = x0;
   y = y0;
   direccion = 1;
   
   estado = PARADO;
   velocidad_salto = 0;
   estado_anterior = estado;

}


// Destructor

Protagonista::~Protagonista() {

  1. ifndef DEBUG
   cout << "Protagonista::~Protagonista()" << endl;
  1. endif

}

// Funciones que implementan el diagrama de estados

void Protagonista::estado_parado(void) {

   // Estando parado podemos realizar las 
   // siguientes acciones
   
   if(teclado->pulso(Teclado::TECLA_IZQUIERDA)) {

direccion = -1; estado = CAMINAR;

   }
   if(teclado->pulso(Teclado::TECLA_DERECHA)) {

direccion = 1; estado = CAMINAR;

   }
   if(teclado->pulso(Teclado::TECLA_GOLPEAR))

estado = GOLPEAR;

   if(teclado->pulso(Teclado::TECLA_SALTAR)) {

velocidad_salto = -5; estado = SALTAR;

   }

}


void Protagonista::estado_caminar(void) {

   // Acciones que podemos realizar
   // mientras caminamos
   animaciones[estado]->avanzar();
   
   mover_sobre_x(direccion * 2);
   if(direccion == 1 && ! teclado->pulso(Teclado::TECLA_DERECHA))

estado = PARADO;

   if(direccion == -1 && ! teclado->pulso(Teclado::TECLA_IZQUIERDA))

estado = PARADO;

   if(teclado->pulso(Teclado::TECLA_GOLPEAR))

estado = GOLPEAR;

   if(teclado->pulso(Teclado::TECLA_SALTAR)) {

velocidad_salto = -5; estado = SALTAR;

   }
   if(!pisa_el_suelo()) {

velocidad_salto = 0; estado = SALTAR;

   }

}


void Protagonista::estado_disparar(void) {

   // Cuando golpeamos nos detenemos
   if(animaciones[estado]->avanzar())

estado = PARADO;

}


void Protagonista::estado_saltar(void) {

   // Acciones que podemos realizar al saltar
   velocidad_salto += 0.1;
   
   if(teclado->pulso(Teclado::TECLA_IZQUIERDA)) {

direccion = -1; mover_sobre_x(direccion * 2);

   }
   
   if(teclado->pulso(Teclado::TECLA_DERECHA)) {

direccion = 1; mover_sobre_x(direccion * 2);

   }


   // Cuando la velocidad cambia de signo empezamos a caer
   if(velocidad_salto > 0.0) {

y += altura((int) velocidad_salto);

if(animaciones[estado]->es_primer_cuadro()) animaciones[estado]->avanzar();

if(velocidad_salto >= 1.0 && pisa_el_suelo()) estado = PARADO;

if(y > ALTO_VENTANA * 2) { estado = MORIR; velocidad_salto = -5; }

   }
   else {

y += (int) velocidad_salto;

   }

}


void Protagonista::estado_morir(void) {

   // Morimos
   velocidad_salto += 0.1;
   y += (int) velocidad_salto;
   
   mover_sobre_x(direccion * 2 * - 1);
   
   animaciones[estado]->avanzar();
   
   if(y > ALTO_VENTANA * 2 + 300) // Salimos de la pantalla

reiniciar(); } </cpp>

En el constructor de la clase hacemos uso del constructor de la clase base de ésta para inicializar los atributos heredados. En el resto de este método se inicializan los atributos de la clase y preparan las animaciones que vamos a utilizar según el estado del protagonista. Al terminar el constructor se reinicia el estado del protagonista para que esté en un estado inicial.

El método actualizar() tiene una implementación algo más extensa de lo que estamos acostumbrados. Lo primero que se hace en el método es establecer la posición de la ventana del nivel. Se toma la posición del protagonista como centro de dicha ventana. Seguidamente si el personaje ha cambiado de estado se reinicia la animación para comenzar con el nuevo tipo de ésta. Lo siguiente que nos encontramos es la implementación del autómata por el método de los cases. Según sea el estado en el que nos encontramos entramos en un caso u otro y realizamos unas determinadas acciones que están incluidas en cada de uno de las funciones que hemos definido con tal fin y que estudiaremos a continuación.

El método colisiona_con() hace que el protagonista muera reproduciendo una animación que hemos creado con este fin mientras que el método reiniciar() de esta clase reestablece todos los atributos a su estado general: la posición, el estado, el salto, la dirección...

El resto de la implementación de la clase concierne al desarrollo de la codificiación del autómata del personaje principal. El diagrama de estados o transiciones de este es el mismo que estudiamos en capítulos anteriores simplificando su implementación.

El método estado_parado() se corresponde con el diagrama cuando el personaje está parado. Cuando está ne este estado el personaje puede caminar hacia la izquierda, hacia la derecha, golpear o saltar. Según sea la tecla que pulsemos entraremos en un caso u otro dentro la misma. Los demás métodos tienen un razonamiento análogo respondiendo al diagrama de transisicones.

En el caso de estado_morir() ya no podemos realizar ninguna acción más por lo que ponemos en marcha la animación del estado y cuando el personaje haya salido de la ventana el doble de su altura reiniciaremos el protagonista.


[editar] La clase Sonido

La clase sonido nos proporciona todo aquello que necesitamos para introducir sonidos de acción en nuestra aplicación. Con ayuda de la galería podemos gestionar todo lo referente a estos sonidos.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Sonido
Clase Sonido

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Sonido.h // // Clase para facilitar el trabajo con sonidos

  1. ifndef _SONIDO_H_
  2. define _SONIDO_H_
  1. include <SDL/SDL.h>
  2. include <SDL/SDL_mixer.h>

class Sonido {

public:
   // Constructor
   Sonido(char *ruta);
   void reproducir();


   // Destructor
   ~Sonido();
private:
   Mix_Chunk *sonido;

};

  1. endif

</cpp>

Como puedes ver en la definición de la clase utilzamos la librería auxiliar SDL_mixer} para poder manejar más cómodamente estos sonidos. La clase consta del destructor y el destructor pertinentes y un método que nos permite reproducir este sonido una vez en el juego. Los sonidos de acciones no suelen ser iterativos y como nosotros no vamos a necesitar que se produzcan repetidamente no vamos a necesitar en este método ningún parámetro que especifique dicho número de repeticiones.

Veamos la implementación de la clase:

<cpp> // Listado: Sonido.cpp // // Implementación de la clase Música

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

using namespace std;


Sonido::Sonido(char *ruta) {

   // Cargamos el sonido
   sonido = Mix_LoadWAV(ruta);
   if(sonido == NULL) {

cerr << "Sonido " << ruta << " no disponible" << endl; exit(1);

   }
   Mix_AllocateChannels(1);
  1. ifdef DEBUG
   cout << "Sonido cargado" << endl;
  1. endif

}


void Sonido::reproducir() {

   Mix_PlayChannel(-1, sonido, 0);

}


Sonido::~Sonido() {

   Mix_FreeChunk(sonido);
   

} </cpp>

El constructor de la clase carga el fichero de sonido especicado como parámetro y reserva un canal de audio. El método reproducir hace sonar este chunk por todos los canales una vez. El destructor de la clase libera los recursos reservados para dicho sonido.

[editar] La clase Teclado

Para no complicar más la implementación del videojuego y como ya hicimos un repaso profundo sobre el manejo del joystick en el capítulo correspondiente a su estudio vamos a definir como elementos de entrada del videojuego al ratón y al joystick. Con esta clase vamos a dotar de toda la potencia que necesitamos para recibir la entrada de teclado.

El diseño de la clase del que partimos para la implementación es el de la figura..

Clase Teclado
Clase Teclado

Vamos a estudiar la definición de la clase:

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


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

using namespace std;


class Teclado {

public:
   // Teclas a usar en la aplicación
   enum teclas_utilizadas {

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

   };
   // Constructor
   Teclado();
   // Consultoras
   // Actualiza la información del teclado
   void actualizar(void);
   // Informa si una tecla ha sido pulsada
   
   bool pulso(teclas_utilizadas tecla);


private:
   // Para conocer el estado de la pulsación
   // de las teclas en todo momento
   Uint8* teclas;


   // Asocia las teclas que necesitamos a la 
   // constate SDL que la representa
   
   map<teclas_utilizadas, SDLKey> teclas_configuradas;

};

  1. endif

</cpp>

Lo primero que nos encontramos en la definición de la clase es un enumerado que nos permitirá trabajar más cómodamente con las constantes de teclado. Además del constructor la clase tiene dos métodos. El primero es común a muchas de nuestras clases. Se trata actualizar() y se encarga de renovar la información que tenemos del teclado.

El segundo, el método pulso(), nos permite conocer el estado de una tecla que le pasamos como parámetro. Este método consultivo será el que más utilicemos ya que tendremos que reaccionar a la pulsación de determinadas teclas en determinados momentos.

En la parte privada de la clase tenemos una variable teclas con el estado del teclado y una aplicación implementada mediante un map que nos permite tener almacenada la relación entre las constantes de SDL y las teclas que tenemos configuradas en nuestro enumerado.

Vamos a ver la implementación de la clase:

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

  1. include <iostream>
  2. 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_GOLPEAR] = 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(teclas_utilizadas tecla) {

   // Comprobamos si una tecla está pulsada
   if(teclas[teclas_configuradas[tecla]])

return true;

   else

return false; } </cpp>

El constructor de la clase relaciona las constantes SDL mediante el map con el enumerado que hemos definido para manejar el teclado. El método actualizar() utiliza la función SDL_GetKeyState() para renovar el estado del teclado. El método pulso() simplemente consulta el estado de la tecla que recibe como parámetro que es un elemento del enumerado.


[editar] La clase Texto

La clase texto nos permite construir frases con las fuentes almacenadas en una imagen que nos permiten hacer rótulos para nuestro juego.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Texto
Clase Texto


Vamos a estudiar la definición de la clase:

<cpp> // Listado: Texto.h // // Con esta clase controlamos los textos con los que va a trabajar la // aplicación


  1. ifndef _TEXTO_H_
  2. define _TEXTO_H_
  1. include <SDL/SDL.h>
  2. include "CommonConstants.h"

class Fuente;


class Texto {

public:
   // Constructor
   Texto(Fuente *fuente, int x, int y, char *cadena);
   void dibujar(SDL_Surface *pantalla);
   void actualizar(void);
   
private:
   
   int x, y;
   
   Fuente *fuente;
   char texto[MAX_TAM_TEXTO];

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varios elementos. Los atributos x e y nos permiten almacenar la posición de la imagen. La variable texto nos permite almacenar el texto a construir mientras que el atributo fuente asocia el texto con la fuente a utilizar.

En la parte pública tenemos elementos cuya funcionalidad es común a la mayoría de las clases de nuestra aplicación. Se trata de dibujar() que muestra (copia) el texto generado en una superficie de SDL y el método actualizar() que renueva el estado de dicho texto cuando se llama a dicho método.

Vamos a ver la implementación de la clase:

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

  1. include <iostream>
  1. include "Fuente.h"
  2. include "Control_Movimiento.h"
  3. include "Texto.h"

using namespace std;


Texto::Texto(Fuente *fuente, int x, int y, char *cadena) {

  1. ifdef DEBUG
   cout << "Texto::Texto()" << endl;
  1. endif
   // Iniciamos las variables
   this->fuente = fuente;
   this->x = x;
   this->y = y;
   // Copiamos la cadena a nuestro texto
   strcpy(texto, cadena);

}


void Texto::dibujar(SDL_Surface * pantalla) {

   // Dibujamos el texto en pantalla
   fuente->dibujar(pantalla, texto, x, y);

}


void Texto::actualizar(void) {

   // No realiza ninguna acción en particular

} </cpp>

El constructor de la clase inicializa los atributos de la misma y copia la cadena que recibe como parámetro en el atributo texto de la clase. El método dibujar utiliza el implementado en la clase fuente para dibujar el texto creado en pantalla.

En cuanto al método actualizar() no realizamos ninguna acción ya que lo hemos dejado preparado por si el lector quiere agregar algún tipo de efecto al texto de los menús.



[editar] La clase Universo

La clase Universo establece el lazo de unicón entro todas las capacidades implementadas en las diferentes clases. Su principal función es ejecutar el bucle del juego y controlar aspectos como la sincronización del videojuego, la salida del juego o la presentación en modo ventana o pantalla completa.

El diseño de la clase del que partimos para la implementación es el de la figura.

Clase Universo
Clase Universo

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Universo.h // // Nos permite cohesionar todas las clases de la aplicación y // nos permite llevar un control global sobre la misma


  1. ifndef _UNIVERSO_H_
  2. define _UNIVERSO_H_
  1. include <SDL/SDL.h>
  2. include <SDL/SDL_mixer.h>
  1. include "Teclado.h"
  2. include "Interfaz.h"


// Declaraciones adelantadas

class Galeria; class Juego; class Editor; class Menu;


class Universo {

public:
   
   // Constructor
   Universo();


   // Proporciona la "reproducción continua"
   void bucle_principal(void);
   
   // Nos permite el cambio entre "opciones" del juego
   void cambiar_interfaz(Interfaz::escenas nueva);


   // Dibuja un rectángulo en pantalla
   void dibujar_rect(int x, int y, int w, int h, Uint32 color = 0);


   // Finaliza el juego
   void terminar(void);
   
   ~Universo();// Destructor 
   
   Teclado teclado;  // Controla el dispositivo de entrada
   Galeria *galeria; // Almacena todas las imágenes necesarias
   SDL_Surface *pantalla; // Superficie principal del videojuego


private:
   // Escena en la que estamos
   
   Interfaz *actual;


   // Pantallas del juego
   
   Juego *juego;
   Editor *editor;
   Menu *menu;
   // Variable que modificamos cuando queremos salir de la aplicación
   bool salir;
   
   void iniciar_ventana(bool fullscreen);
   void pantalla_completa(void);


   // Estudia los eventos en un momento dado
   int procesar_eventos(void);
   
   // Lleva el control del tiempo
   int sincronizar_fps(void);

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varios atributos que nos permiten asociar a la clase directamente con las clases Interfaz, Juego, Editor y Menu. Estas son las clases principales, después de Universo, en la jerarquía de la aplicación. Además de estos atributos posee otro, salir, que será marcado a verdadero cuando el usuario quiera terminar con la aplicación.

Además de estos atributos la clase tiene una serie de métodos privados que estudiaremos con la implementación de la clase.

En la parte pública de la clase tenemos varios atributos que nos permiten asociar la clase con el teclado, la galería multimedia y la pantalla o superficie principal del juego. Además tenemos una serie de métodos para llevar a cabo las tareas básicas de la aplicación que pasamos a detallar con la implementación:

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

  1. include <iostream>
  1. include "Universo.h"
  2. include "Juego.h"
  3. include "Editor.h"
  4. include "Menu.h"
  5. include "Galeria.h"
  6. include "CommonConstants.h"


using namespace std;


Universo::Universo() {

  1. ifdef DEBUG
   cout << "Universo::Universo()" << endl;
  1. endif
   iniciar_ventana(false);          // Iniciamos en modo ventana
   
   galeria = new Galeria;           // Creamos todo lo necesario
   
   juego = new Juego(this);
   editor = new Editor(this);
   menu = new Menu(this);
   
   salir = false;                   // No queremos salir (todavía)
   
   actual = juego;                  // Establecemos escena actual
   cambiar_interfaz(Interfaz::ESCENA_MENU);   // Cambiamos a menu

}


void Universo::bucle_principal(void) {

   int rep;


   // Bucle que realiza el polling
   while(salir == false && procesar_eventos()) {

teclado.actualizar(); // Tomamos el estado del teclado

rep = sincronizar_fps(); // Sincronizamos el tiempo

// Actualizamos lógicamente

for(int i = 0; i < rep; i ++) {

actual->actualizar(); // Actualizamos la escena actual

}

// Limpiamos la pantalla

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

// Actualizamos las imágenes

actual->dibujar();

   }

}


void Universo::iniciar_ventana(bool fullscreen) {

   int banderas = 0;
   // Iniciamos todos los subsistemas
   if(SDL_Init(0) < 0) {

cerr << "Universo::iniciar_ventana:" << SDL_GetError() << endl; exit(1);

   }
   // Al salir, cerramos libSDL
   atexit(SDL_Quit);
   if(fullscreen)

banderas |= SDL_FULLSCREEN;

   banderas |= SDL_HWSURFACE | SDL_DOUBLEBUF;


   // Establecemos el modo de video
   pantalla = SDL_SetVideoMode(ANCHO_VENTANA, ALTO_VENTANA, BPP, banderas);
   if(pantalla == NULL) {

cerr << "Universo::iniciar_ventana:" << SDL_GetError() << endl; exit(1);

   }
   SDL_WM_SetCaption("Wiki libSDL", NULL);
   // Ocultamos el cursor
   SDL_ShowCursor(SDL_DISABLE);
   // Inicializamos la librería SDL_Mixer
   if(Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT,\
                    1, 2048) < 0) {
       cerr << "Subsistema de Audio no disponible" << endl;
       exit(1);
   }
   // Al salir cierra el subsistema de audio
   atexit(Mix_CloseAudio);

}


int Universo::procesar_eventos(void) {

   static SDL_Event event; // Con "memoria"


   // Hacemos el polling de enventos
   while(SDL_PollEvent(&event)) {

// Se estudia

switch(event.type) {

case SDL_QUIT: return 0;

case SDL_KEYDOWN:

// Tecla de salida

if(event.key.keysym.sym == SDLK_q) return 0;

// Tecla paso a pantalla completa

if(event.key.keysym.sym == SDLK_f)

pantalla_completa();

break;

default:

// No hacemos nada

break;

}

   }
   return 1;

}


void Universo::pantalla_completa(void) {

   // Alterna entre pantalla completa y ventana
   SDL_WM_ToggleFullScreen(pantalla);

}


int Universo::sincronizar_fps(void) {

   static int t0;
   static int tl = SDL_GetTicks();
   static int frecuencia = 1000 / 100;
   static int tmp;
  1. ifdef FPS
   static int fps = 0;
   static int t_fps = 0;
  1. endif
   // Tiempo de referencia
   t0 = SDL_GetTicks();
  1. ifdef FPS
   // Actualizamos información cada segundo
   if((t0 - t_fps) >= 1000) {

cout << "FPS = " << fps << endl; fps = 0; t_fps += 1000 + 1;

   }
   fps++;
  1. endif
   // Estudio del tiempo
   if((t0 - tl) >= frecuencia) {

tmp = (t0 - tl) / frecuencia; tl += tmp * frecuencia; return tmp;

   }
   else {

// Tenemos que esperar para cumplir con la frecuencia

SDL_Delay(frecuencia - (t0 - tl)); tl += frecuencia;

return 1;

   }

}


void Universo::cambiar_interfaz(Interfaz::escenas nueva) {

   Interfaz *anterior = actual;
   
   // Según sea la escena a la que queremos cambiar
   
   switch(nueva) {
    case Interfaz::ESCENA_MENU:

actual = menu; break;

    case Interfaz::ESCENA_JUEGO:

actual = juego; break;

    case Interfaz::ESCENA_EDITOR:

actual = editor; break;

   }
   if(anterior == actual) {

cout << "Universo: Cambia a la misma escena" << endl;

   }
   // Una vez cambiada a la nueva escena, la reiniciamos
   actual->reiniciar();

}


void Universo::dibujar_rect(int x, int y, int w, int h, Uint32 color) {

   SDL_Rect rect;
   
   // Almacenamos la variable en un rectángulo
   rect.x = x;
   rect.y = y;
   rect.h = h;
   rect.w = w;
   // Dibujamos el rectángulo
   SDL_FillRect(pantalla, &rect, color);

}


// Queremos salir de la aplicación

void Universo::terminar(void) {

   salir = true; // Afecta al polling

}

// Destructor

Universo::~Universo() {

   delete juego;
   delete galeria;
   delete editor;
   delete menu;
   
  1. ifdef DEBUG
   cout << "Universo::~Universo()" << endl;
   cout << "Gracias por jugar" << endl;
  1. endif

} </cpp>

El constructor de la clase inicializa todos los atributos de la misma creando las diferentes escenas y estableciendo la principal para comenzar con la aplicación. El método bucle_principal() es un bucle "infinito" que va actualizando la escena en la que se encuentre la aplicación haciendo una llamada a su método actualizar. Por cada vuelta del bucle se limpia la superficie principal para que sea repintada.

Para sincronizar los fps (cuadros por segundo) se utiliza una función que nos devuelve el número de veces que tenemos que actualizar la escena actual para establecer un margen máximo de fps.

Necesitamos una función que nos permita establecer las propiedades que vamos a utilizar para ejecutar nuestra aplicación. El método iniciar_ventana() establece el modo de video que vamos a utilizar en la aplicación e inicializa el subsistema de audio para que pueda ser utilizado.

Es necesario manejar ciertos eventos que nos permitan interrumpir la aplicación o cambiar, por ejemplo, el formato de la ventana. En procesar_eventos() manejamos aquellos eventos que sean especiales. Nos referimos a eventos especiales cuando hablamos de salir del juego o de cambiar el modo de juego a pantalla completa. La implementación de la gestión del teclado la hemos visto hace un par de clases.

El método sincronizar_fps() es uno de los más interesantes de esta clase. Toma dos marcas de tiempo y realiza una espera activa hasta que se agote el intervalo de tiempo que queremos utilizar para que pueda realizarse alguna acción más. Este método es el encargado de la temporización del juego marcando la barrera de 100 fps como la aceptable para ejecutar el videojuego con buena fluidez.

En el juego podremos cambiar de escenario en distintos momentos. Por ejemplo si nos encontramos en el menú principal podremos entrar en el editor de niveles o a jugar un partida. Si estamos jugando podremos salir al menú pulsando una tecla. De este cambio de escenario se encarga el método cambiar_interfaz() que hace un cambio de escena a la que recibe como parámetro. Una vez realizado el cambio reinicia la escena para partir de su estado inicial.

Necesitamos dibujar rectángulos sobre una superficie que nos permita crear fondos y limpiar la pantalla principal por cada vuelta del bucle. El método que se encarga de esta tarea es dibujar_rect(). La implementación de este método no tiene ningún secreto, hace uso de la función SDL_FillRect() para rellenar un cuadro en una superficie.

Los dos últimos métodos son triviales. El método terminar() nos permite indicar a la clase Universo que queremos acabar con la aplicación marcando la bandera salir a true mientras que el destructor de la clase acaba con los recursos acaparados por dicha clase en el constructor.



[editar] La clase Ventana

La clase ventana proporciona una funcionalidad fundamental dentro de la aplicación. Nos permite establecer una cámara en forma de ventana que va a ir siguiendo nuestro personaje durante el transcurso del juego y que nos permitirá movernos por todo el nivel cuando estemos usando el editor para crear o modificar niveles.

El diseño de la clase del que partimos para la implementación es el de la figura.


Imagen:5Ventana.png
Clase Ventana

Vamos a estudiar la definición de la clase:

<cpp> // Listado: Ventana.h // // Esta clase nos permite navegar por las superficies de los niveles

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

class Ventana {

public:
   // Constructor
   Ventana(int filas, int columnas);
   
   void actualizar(void);
   void establecer_pos(int x, int y);
   void tomar_pos(int * x, int * y);
   void tomar_pos_final(int * x, int * y);
   
   int pos_x(void);
   int pos_y(void);
   
private:
   int x, y;
   int x_final, y_final;
   int limite_x, limite_y;
   
   void limitar_movimiento(void);

};

  1. endif

</cpp>

En la parte privada de la clase tenemos varios atributos que nos permiten establecer un control sobre el movimiento de la ventana. (x ,y) informan acerca de la posción actual de la ventana mientras que x_final e y_final hacen lo propio sobre la posición a la que queremos llegar con dicha ventana.

El método privado limitar_movimiento() nos permite que la ventana no se nos salga del tamaño del nivel para que no se nos muestren partes que no deben de ser cargadas en pantalla. Veamos la implementación de la clase.

<cpp>V // Listado: Ventana.cpp // // Implementación de la clase ventana

  1. include <iostream>
#include "Ventana.h"
#include "Nivel.h"
using namespace std;


Ventana::Ventana(int filas, int columnas) {
    // Inicializamos las variables
    x = y = 0;
    x_final = y_final = 0;
    limite_x = ANCHO_VENTANA * 2 - columnas * TAMANO_BLOQUE;
    limite_y = ALTO_VENTANA * 2 - filas * TAMANO_BLOQUE;
}


void Ventana::actualizar(void)
{
    int incremento_x = x_final - x;
    int incremento_y = y_final - y;
    // Si existe variación
    if(incremento_x != 0) {

// Controlamos el movimiento de la ventan if(abs(incremento_x) >= 10) x += incremento_x / 10; // Reducimos la cantidad de movimiento

else // Sobre todo en movimientos pequeños x += incremento_x / abs(incremento_x);

    }
    // Si existe variación
    if(incremento_y != 0) {

// Animación de movimiento fluida if(abs(incremento_y) >= 10) y += incremento_y / 10; else y += incremento_y / abs(incremento_y);

    }
}


// Funciones referentes al posicionamiento

void Ventana::establecer_pos(int x, int y) {
    x_final = x;
    y_final = y;
    limitar_movimiento();
}


void Ventana::tomar_pos_final(int *x, int *y) {
    *x = x_final;
    *y = y_final;

}


void Ventana::tomar_pos(int *x, int *y) {

   *x = this->x;
   *y = this->y;

}


int Ventana::pos_x(void) {

   return x;

}

int Ventana::pos_y(void) {

   return y;

}


void Ventana::limitar_movimiento(void) {

   // Comprobamos que se cumplen los límites lógicos de pantalla
   if(x_final < 0)

x_final = 0;

   if(y_final < 0)

y_final = 0;

   if(x_final > limite_x)

x_final = limite_x;

   if(y_final > limite_y)

y_final = limite_y; }

</cpp>

El constructor de la clase establece la posición inicial de la ventana así como fija el límite de la misma en el eje vertical y horizontal. El método actualizar() realiza un moviento progresivo hasta el destino final del a ventana. Si existe variación en alguno de los ejes y dependiendo de la distancia a la que se encuentre del objetivo realizará un movimiento más preciso o uno com mayor desplazamiento. Podemos ver como existe para cada uno de los ejes una comprobación sobre si la ventana está en la posición final y de no ser así, dependiendo del tamaño del incremento de posición que tenga que realizar avanzará más o menos.

El método establecer_pos() nos permite determinar la posición de destino hacia donde ha de moverse la ventana mientras que los métodos tomar_pos(), pos_y(), pos_x() nos permiten conocer la posición actual y final de la ventana.

El método limitar_movimiento() comprueba si la posición final de la ventana se sale del rango del tamaño del nivel para evitar movimientos no permitidos. La manera de realizar la comprobación es muy simple basta con comprobar que el destino final es mayor que la posición (0, 0) pero menor que el límite máximo de la ventana en los dos ejes.

[editar] Recopilando

Como puedes observar para realizar una pequeña aplicación hemos tenido que desarrollar un duro trabajo. Quizás este apartado ha sido demasiado formal pero era necesario que vieses que un proceso de desarrollo conlleva de la propia codificación.

En este capítulo hemos integrado todos los aspectos vistos durante el curso. Ahora te toca a tí crear tu aplicación

Herramientas personales