El Juego de la Serpiente

Objetivos

Juego
Aspecto del juego de la serpiente, tema del presente artículo.

El objetivo de este artículo 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: por una parte nos obliga a crear programas eficientes y rápidos, y por otra 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 a veces 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 bibliotecas 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 bibliotecas 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.

De todos modos, procuraremos que las referencias directas al API de Windows se limiten a una clase, de modo que sea relativamente sencillo adaptar el juego a otros sistemas.

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 ordenador.

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 se podrá elegir entre una lista de mapas prediseñados.
  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 un obstáculo o con el propio cuerpo de la serpiente.

Crear un programa orientado a objetos

Como estamos haciendo un programa C++, en este caso 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

Objetos
Diseño de objetos

En las aplicaciones C++ se puede usar programación orientada a objetos. En ese caso 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á al objeto comida si ocupa una celda concreta antes de ocuparla, y por lo tanto si debe 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 se ha 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.