Programar un juego para Windows con C++

El juego de la serpiente



Aspecto del juego de la serpiente, tema de este post.

Objetivos:
El objetivo de este post es ilustrar por una parte el modo en que se diseña una aplicación orientada a objetos, y por otra el uso del API de Windows.

Aunque un juego parezca una forma frívola de practicar programación, en realidad tiene varias ventajas, ya que nos obliga a crear programas eficientes y rápidos, y también a resolver problemas complejos.

Primer acercamiento:
Lo primero que hay que hacer cuando se afronta la resolución de cualquier problema mediante un programa de ordenador (y hay que tener en cuenta que un juego también es un problema de programación), es analizarlo con detenimiento.

El análisis es la fase más importante y compleja de la resolución de cualquier problema. Un problema bien analizado será mucho más fácil de programar, y mucho más rápido de codificar y de ejecutar.

Con los juegos tenemos un problema añadido: tendremos que crear programas en los que las tanto las respuestas al usuario como las salidas por pantalla se ejecuten muy rápidamente. La dinámica de los juegos es muy importante, y de ella depende en gran medida el que un juego resulte atractivo o no.

Por lo tanto, tendremos que resolver dos tipos de problemas: por una parte problemas con los algoritmos del juego concreto, tal como en cualquier otro tipo de programa; por otra parte, problemas relacionados con el sistema operativo y el hardware. En muchas ocasiones, los primeros estarán supeditados a los segundos, y tendremos que modificar los algoritmos para que se adapten al hardware, si es necesario.

Debido a estas peculiaridades, los juegos rara vez se pueden programar recurriendo sólo al C++ estándar. El ejemplo más evidente es la lectura del teclado. En C++ estándar disponemos streams para leer el teclado, pero están en un nivel con respecto al usuario demasiado alto. Esto debe ser así para permitir que se pueda editar un texto antes de validarlo como entrada. Si estamos introduciendo una cadena, siempre podemos borrar caracteres y corregir antes de pulsar el retorno de línea.

Cuando se programa un juego normalmente será preferible procesar las pulsaciones de teclas a medida que se produzcan, no querremos pulsar la tecla de "flecha arriba", y después el retorno de línea cada vez que nuestro personaje deba moverse hacia arriba.

Las librerías estándar no poseen funciones para la detección de teclas individuales, por lo tanto tendremos que crear nuestras propias funciones para eso, y en general, estas funciones dependerán en gran medida del sistema operativo, o incluso de la máquina.

Lo mismo pasa con la pantalla. Los juegos requieren colores, gráficos de alta resolución, rapidez de actualización, etc. Todas estas características no pertenecen a librerías estándar, y por lo tanto, los juegos estarán ligados siempre a determinados sistemas operativos.

Para este artículo hemos elegido el sistema operativo Windows, y usaremos el API32 para acceder a funciones relacionadas con el hardware: teclado, ratón, gráficos y sonido.

El juego:
Primero estableceremos las reglas del juego. Hemos escogido deliberadamente un juego sencillo y conocido, de modo que todos (o casi todos) sabremos de qué hablamos en cada momento.

Para hacerlo algo más complicado, hemos elegido un juego de habilidad, en lugar de uno de tablero, ya que los tiempos de respuesta de estos últimos juegos no son tan críticos, y permiten mucha más flexibilidad a la hora de llevarlos a programa.

Las reglas del juego de la Serpiente son sencillas:

   1. Jugaremos en un laberinto de dos dimensiones y de un tamaño adecuado para que quepa en la pantalla. El tamaño dependerá de la pantalla, y por lo tanto del hardware, aunque en este caso será de un tamaño arbitrario que definiremos nosotros mismos.
   2. El mapa del laberinto dependerá del nivel de juego, siendo en los niveles más bajos una única sala rodeada de un muro, y apareciendo más obstáculos a medida que el nivel vaya subiendo.
   3. El personaje es una serpiente que se moverá por el laberinto según las órdenes del jugador. Este sólo dispone de las teclas del cursor para modificar la dirección en que se mueve la serpiente, y ésta permanecerá moviéndose en la misma dirección mientras el jugador no decida cambiarla. La serpiente nunca podrá detenerse.
   4. Siempre existirá en pantalla un objeto comestible. El objetivo del juego es hacer comer a la serpiente tantos de estos objetos como sea posible. Cada vez que la serpiente se coma uno de estos objetos, automáticamente aparecerá uno nuevo. Para hacer el juego más atractivo, a intervalos irregulares de tiempo aparecerán otros objetos comestibles de vida limitada, pero que proporcionan más puntos.
   5. Cada vez que la serpiente se come un objeto crecerá.
   6. Si la serpiente choca con una de las paredes o con su propio cuerpo muere.

Como hemos comentado, el objetivo es comer tantos objetos comestibles como sea posible. La serpiente crece con cada comida, por lo tanto, la dificultad del juego aumenta a medida que crece la serpiente, puesto que será más difícil encontrar un camino hasta la siguiente comida sin chocar con el propio cuerpo de la serpiente.

Crear un programa orientado a objetos:
Como estamos haciendo un programa C++, usaremos clases para cada concepto u objeto del programa. Por ejemplo, crearemos una clase para "Serpiente" con los datos que definan sus propiedades y con las funciones necesarias para manipularlos.

Los principales objetos que definiremos serán los siguientes:

    * Juego: datos y funciones para manejar el juego completo.
    * Serpiente: datos y funciones para manejar la serpiente.
    * Laberinto: datos y funciones para manejar el tablero.
    * Tanteo: puntuaciones.
    * Comida: manipulación de objetos comestibles.
    * Extra: manipulación de objetos comestibles extras de vida limitada.
    * Gráficos: visualizaciones gráficas del juego.

Otros objetos que necesitaremos:

    * Coordenada: tratamiento de coordenadas.
    * Direccion: tratamiento de direcciones de movimiento.
    * Sección: tratamiento de secciones de serpiente.
    * Cola: plantilla para creación de colas, en nuestro caso, una cola de secciones de serpiente.

Objetos y comunicaciones entre ellos:



En las aplicaciones C++ se usa programación orientada a objetos. Se deben definir los objetos necesarios así como los mensajes que se cruzan entre ellos.

En el diagrama que vemos arriba se representan mediante rectángulos los distintos objetos. Algunos de ellos contienen como parte de si mismos otros objetos auxiliares.

Las flechas representan intercambios de mensajes, y la dirección indica quién los envía y quién los recibe y procesa.

Estas comunicaciones son las siguientes:

    * Colisión: la serpiente puede preguntar al laberinto si el contenido de la celda a la que se va a mover está libre o no.
    * Avanzar: la serpiente avanzará una posición en función de la temporización y de las entradas del jugador.
    * Posición: la serpiente también preguntará si el objeto comida si ocupa una celda concreta antes de ocuparla, y por lo tanto comer su contenido.
    * Situar: la serpiente será la encargada de colocar un nuevo objeto comida cada vez que se coma el actual.
    * Situar: el objeto Extra también tiene un procedimiento para situarlo en pantalla, pero en este caso no será la serpiente la encargada de colocarlo, sinó el propio juego, ya que no estará siempre en pantalla.
    * ObtenerLibre: la comida y el extra consultarán con el laberinto si una casilla está libre, para reponer el objeto comida o extra cuando sea necesario. Las casillas libres serán las que no estén ocupadas por muros, la comida o por la propia serpiente.
    * Actualizar: también tendremos que mantener un tanteo, e incrementarlo cada vez que la serpiente coma.
    * Mostrar: todos los objetos que tengan representación en pantalla tendrán que comunicarse con el objeto "Gráficos", para actualizar dicha representación.
    * Iniciar: tendremos funciones de iniciar para "Serpiente", "Laberinto" y "Tanteo".

Todo esto es una estructura lógica, poco a poco veremos como implementar estas comunicaciones dentro de nuestro programa.

También es posible que algunas de estas características no sigan la secuencia más lógica. Por ejemplo, una de las cosas que he cambiado es la forma de averiguar si la serpiente ocupa una casilla determinada. Originalmente tuve la idea de que fuera la serpiente quien dijera si ocupa o no una casilla concreta, pero de ese modo estaría obligada a consultar las coordenadas de todas sus secciones, esto se usaría tanto para obtener una casilla libre para situar la comida, como para verificar si la serpiente ha chocado consigo misma.

Esta forma de trabajar tiene consecuencias no deseadas, ya que esas consultas dependerán de la longitud de la serpiente, y pueden afectar a la velocidad con la que se desarrolla el juego.

De modo que haremos que esa tarea la desarrolle el objeto "Laberinto". Cada vez que actualicemos la posición de la "Serpiente", añadiendo o eliminando secciones, lo notificaremos al objeto "Laberinto", lo mismo harán los objetos "Comida" y "Extra", y será el "Laberinto" el que se encargará de mantener esa información en una matriz. Los objetos "Serpiente", "Comida" y "Extra" consultarán con "Laberinto" para saber el contenido de una casilla, y como esta consulta se hará a través de una matriz, el tiempo de respuesta será constante y mucho más rápido que consultar una lista dinámica.