Postmortem (II)

Problemas con el módulo de red

A partir del diagrama de módulos, empezamos a trabajar ya en algunas clases. Primeramente se decidió desarrollar el módulo de comunicación por red. Dicho módulo debía ser el primero en construirse puesto que es la base para el sistema cliente/servidor de nuestro juego.

Para construir este módulo, Daniel utilizó la API DirectPlay de DirectX. Se eligió este método frente a otros debido a que está orientado a los videojuegos (multijugador) y a que a priori parecía bastante sencillo de utilizar. Sin embargo, programar con esta API resultó ser bastante complicado conforme avanzaba el proyecto, y prueba de ello es que se dedicaron cerca de dos meses de la duración total del proyecto a la codificación y prueba de este módulo.

El principal problema que presenta DirectPlay es la casi total ausencia de documentación que existe sobre el tema. Apenas existen tutoriales y los pocos que hay o bien están obsoletos (versiones anteriores de DirectX a la que se utilizó en el proyecto, la 9.0) o bien están diseñados para utilizar otro tipo de lenguaje de programación diferente al C++ (por ejemplo Visual Basic). De esta forma, sólo se podía recurrir a la documentación proporcionada por Microsoft en el SDK de DirectX, que en su mayor parte se limita a la descripción de las funciones que proporciona la API. También se utilizaron los tutoriales que venían en el SDK, pero generalmente eran bastante difíciles de entender. Con todo ello, finalmente se consiguió desarrollar un módulo de red basado en DirectPlay que permitía encapsular toda la gestión y el acceso a la red. Hacia principios de Febrero de 2004 se contaba con una versión casi definitiva del módulo de red para hacer pequeñas pruebas entre el servidor y el cliente.

Representación de los niveles y creación de los mapas para el juego

Los siguientes módulos que se desarrollaron fueron los relacionados con la gestión de la información de la partida y el gestor de información del mapa. Estos dos módulos son muy parecidos tanto en el cliente como en el servidor, por lo que tratamos de ahorrar código y tiempo programando simultáneamente para ambas partes.

Básicamente cada nivel de juego constituiría en un cubo gigante que a su vez se dividiría en celdas cúbicas, por las que se desplazarían los personajes y en las que se situarían los demás objetos del juego. Debido a ello, y para representar el nivel, desde un primer momento decidimos emplear una estructura de datos consistente en una matriz tridimensional. Cada posición de esa matriz se correspondería con una celda de dicho nivel, y contendría un enlace a una única entidad del mapa que sería precisamente la que ocupara dicha celda.

Inicialmente pareció una buena idea, ya que simplificaba las estructuras de datos a utilizar y la gestión de las mismas. Sin embargo fueron surgiendo algunos problemas que no habíamos contemplado durante la etapa de análisis. Así por ejemplo, las colisiones entre enemigos y jugadores -enormemente simplificadas al utilizar el modelo de una entidad por celda- requerían que en ocasiones ambos personajes se encontrasen en la misma celda, lo cual no era posible en el esquema que adoptamos. Es por esto que algunas veces puede parecer que un enemigo no toca al jugador. Pero este aspecto se puede solucionar fácilmente.

Para almacenar los mapas en sí, se decidió utilizar ficheros de texto claro que almacenasen toda la información mediante una sencilla estructura gramatical propia. Posteriormente se cargarían en el juego mediante un sencillo procesador de lenguaje que analizaría la estructura del fichero antes de cargarlo, y si este contenía errores, indicarlo y finalizar su carga. El hecho de utilizar ficheros de texto para almacenar los mapas nos permitía editar los mapas manualmente con un simple editor de textos. El único inconveniente que tiene este método de creación de niveles es que se pueden cometer errores fácilmente (por ejemplo colocar en la misma celda un objeto y un bloque) y a su vez complica mucho la construcción de mapas de grandes dimensiones. Por esta razón incorporamos al proceso una mejora que nos permitió acelerar la generación de mapas para el juego.

ConvASCComo no se tenía tiempo suficiente para realizar un editor de mapas completo, empleamos una herramienta de modelado como 3DS Max para crear la estructura del nivel -mediante formas simples como cubos o esferas-, de modo que se puede percibir visualmente qué aspecto toma el mapa durante su construcción. Una vez diseñado el mapa de esta manera, esa información se exporta a un fichero de texto de tipo ASC (formato común entre los programas de modelado 3D). A partir de dicho fichero se genera el mapa de juego empleando una pequeña utilidad de traducción a la que denominamos ConvASC.

Por su parte el módulo de Información de la partida no presentó muchas dificultades, ya que su función se limita a mantener toda la información general de la partida actualizada. Se trata del módulo de donde se obtiene todos los datos utilizados en la partida.

La gestión de acciones y el retardo de la red

Otro de los módulos que también planteó graves problemas fue el Gestor de acciones. La función principal del gestor de acciones es reducir el lag que se produce en los juegos multijugador. El lag es el retraso que se experimenta al enviar datos a través de la red (en nuestro caso desde el servidor al cliente) y que provoca que un cliente esté de-sincronizado respecto a la información que contiene el servidor.

Quizá el tratamiento de la sincronización del cliente con el servidor fuese uno de los puntos más críticos durante el proceso de desarrollo, si bien en nuestro caso, el estilo de juego permitía “engañar” con mucha más frecuencia de la deseada al jugador que en otro estilo de juegos (como los FPS). Para tratar este aspecto, Iván Lobo organizó en el laboratorio una reunión con Pedro Fernández Ureña -miembro de Pyro Studios-, que conoce bastante bien el tema. De esa reunión obtuvimos una serie de ideas que posteriormente aplicaríamos a nuestro caso.

La forma de comunicar el cliente con el servidor que empleamos en Vórtice es mediante el paso de mensajes, que especifican las pulsaciones de teclas o botones realizadas en el cliente. Estas pulsaciones se examinan en el servidor y si la acción es posible entonces se realiza, modificando los elementos que intervienen en la acción. Por su parte, el servidor comunica periódicamente a todos los clientes la situación actual de todos los elementos del juego (enemigos, bloques, personajes, etc.) y son los clientes los que deben modificar su estado de juego para adaptarlo a la información que le llega desde el servidor. En pocas palabras, se podría decir que la partida se disputa en realidad en el servidor, y que los clientes se limitan a representar el estado del juego en un momento determinado a partir de la información que reciben del servidor.

En Vórtice, la interacción de los personajes con el mundo de juego se realiza mediante acciones, como pueden ser saltar, caminar, caer desde una altura o crear un bloque. Lo que pretendemos es que todas estas acciones se realicen de forma fluida en el cliente (es decir, sin saltos), pero debido al lag esto es algo casi imposible de conseguir puesto que en un momento dado la información existente en el servidor y el cliente va a ser distinta el 99% de las veces. Así, cada vez que se actualiza el cliente con la información que le envía el servidor siempre va a haber un desfase, dando como resultado un salto o cambio brusco de la acción.

¿Cómo solucionar los problemas derivados del lag? Para empezar decidimos emplear un mecanismo que nos permitiese calcular la posición de un personaje cuando realiza una acción en cualquier instante de tiempo, es decir, que conociendo la acción que está realizando un personaje y el instante de tiempo actual fuese posible determinar la posición que ocupará el jugador en ese momento. Inicialmente se pensó en una tabla de valores que nos proporcionara un desplazamiento de la posición del jugador para un instante de tiempo dado. Sin embargo dicha tabla consistiría en un conjunto discreto de valores, lo que no nos permitiría calcular de forma precisa la posición exacta de un personaje. Se necesitaba de un mecanismo continuo: algo así como una función a la que se le suministre un instante de tiempo y nos devuelva el desplazamiento de la posición. Esto nos pareció lo más correcto, pero aún teníamos el problema de calcular esas funciones.

Función de interpolaciónLa solución que elegimos fue la de construir polinomios de interpolación a partir de las tablas de valores que habíamos ideado originariamente. Este método nos permite obtener una función continua que pasa por una serie de puntos determinados (discretos). La función obtenida está parametrizada mediante una única variable de entrada que indica el instante de tiempo para el que se quiere calcular la posición. De este modo ya teníamos el mecanismo base para evitar el lag: el servidor informaría en cada actualización de la acción que realiza el personaje, su posición actual y el tiempo transcurrido desde que inició la acción. Por su parte, el cliente calcularía el desplazamiento desde la posición actual en función de ese tiempo transcurrido.

Si bien esta idea era buena, aún existía un problema: entre dos acciones llevadas a cabo por un personaje se podría producir una actualización del servidor (con una nueva acción). En ese caso el cliente modificaría el estado de acción del personaje al nuevo estado y posición indicados por la actualización del servidor, produciéndose un salto desde la posición registrada en el cliente a la nueva posición indicada por el servidor. Para compensar este problema se decidió hacer efectiva la actualizar sólo cuando la posición enviada por el servidor indicase otra celda distinta a la de la posición en el cliente. También decidimos aumentar el intervalo de actualización por parte del servidor a un periodo de aproximadamente la media de la duración de cada una de las posibles acciones que pueden llevar a cabo los personajes.

Los comentarios están cerrados.