Sincronización entre jugadores en videojuegos online

Existen varias técnicas para solventar el problema de la sincronía de información entre jugadores. Desde sistemas de comandos hasta predicción de los movimientos.

En la actualidad existen numerosas técnicas que los que nos dedicamos al desarrollo web aplicamos a nuestros desarrollos para que nuestras aplicaciones vayan lo más rápido posible: compresión de imágenes, uso de CDNs, reducción del tamaño del código Javascript y CSS (minify), compresión GZIP en servidor, etc. Es decir: queremos que la web cargue lo más deprisa posible.

Sin embargo, cuando necesitamos proporcionar información en tiempo real a nuestros usuarios (Twitter o Facebook son algunos ejemplos) también tenemos que tener en cuenta la latencia y el tiempo de respuesta desde que se realiza una petición hasta que el cliente recibe la información. En aplicaciones web normalitas no es importante para el usuario saber al 100% cuando se ha completado la acción. Por lo general estos tiempos suelen diluirse en el flujo de ejecución utilizando mensajes de confirmación, emoticonos o pequeñas animaciones.

Pero ¿qué ocurre cuando el flujo de envío y recepción de datos es elevado? Este es el caso de los videojuegos online. Por su propia naturaleza de “tiempo real”, los videojuegos online no pueden presuponer que cada paquete enviado ha sido recibido por el receptor correctamente, sino que deben existir mecanismos que aseguren que cada jugador recibe la información necesaria para que su experiencia de usuario no se vea mermada.

En la actualidad existen varias técnicas para asegurar el envío y recepción de datos en los juegos online. En este post explicaré algunas de ellas.

Peer-to-peer lockstep

Este algoritmo era la solución preferida para los juegos multijugador diseñados para ser jugados en red local (LAN), donde el problema era la latencia de red y no tanto el ancho de banda. Juegos como el Age of Empires, el Starcraft o el Command & Conquer utilizaban esta técnica para gestionar el flujo de datos recibido y enviado entre jugadores.

La idea era muy sencilla: el juego está compuesto por turnos y en cada turno se generan una serie de comandos que tendrán una consecuencia directa en el estado del juego. Por ejemplo: mover una unidad, atacar a una tropa o construir un edificio. Lo único que habría que enviar son una serie de comandos y ejecutarlos en el mismo orden en cada máquina conectada a la red. Así, todos los jugadores verían lo mismo al mismo tiempo y daría la sensación de que están jugando en un tablero único.

Juegos RTS como Age of Empires o Starcraft supusieron una revolución
Juegos RTS como Age of Empires o Starcraft supusieron una revolución

Como se podrá suponer, esta es la idea básica y faltarían cientos de matices para hacerla realmente efectiva. Se puede leer más al respecto en este enlace: 1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond.

Pese a la simplicidad del planteamiento, este modelo tenía limitaciones importantes:

  • Es difícil asegurar que un juego sea completamente determinista; por ejemplo, una unidad podría moverse unos pocos píxeles más o llegar unos milisegundos antes a una posición. Estas ligeras diferencias acumuladas a lo largo de una partida podría conllevar una desincronización total de la partida.
  • Todos los comandos indicados por un jugador tendrían que ser recibidos antes de “simular” ese turno. Por tanto, había que suponer que cada jugador tenía una latencia de juego igual al jugador con más latencia (el de peor conexión).
  • Como el estado del juego se iba creando a medida que los jugadores juegan, todos deben haber empezado la partida a la vez por la sincronía necesaria. Esto conllevaba la imposibilidad de unirse a una partida ya empezada, o una reconexión en caso de caída.

Cliente / Servidor

Con la aparición de juegos de acción como Doom, Unreal o Quake, el modelo anterior se volvió completamente inservible. La cantidad de información transmitida era elevada y el número de jugadores superior a los juegos RTS conocidos hasta entonces, lo que resultaba en una latencia sumamente notable por los usuarios.

Si se aplicaba el modelo peer-to-peer lockstep, cuando un jugador de Doom pulsaba una tecla, esta acción era enviada al resto de jugadores para que pudieran simular la acción en sus máquinas, volviendo imposible realizar acción alguna. En otras palabras: antes de poder moverte o disparar, tenías que esperar a que te llegara la información del jugador con la conexión más lenta.

Para solventar este problema se utilizó el paradigma cliente/servidor. Utilizando este método, cada jugador enviaba a una máquina común (servidor) la información de juego y era esta máquina la encargada de sincronizar la partida entre los distintos jugadores.

Quake y Doom fueron de los primeros juegos en utilizar el paradigma cliente/servidor

Esto resultó en un gran avance. La experiencia de juego ahora dependía de la conexión entre el cliente y el servidor en lugar de depender del jugador con peor conexión. También se consiguió que los jugadores pudieran reconectarse ante caídas, o aumentar el número de jugadores simultáneos en partida a medida que se iba aumentando el ancho de banda entre clientes y servidores.

Sin embargo, todo no iba a ser un camino de rosas. El diseño original de la arquitectura cliente/servidor para Doom o Quake asumía que el peor caso tendría una latencia inferior a los 200 milisegundos. Sin embargo, en la práctica esto no ocurría debía a la cantidad de saltos que daban los paquetes hasta que llegaban al servidor (cliente – modem – proveedor – servidor / servidor – proveedor – modem – cliente). Esta latencia se notaba muchísimo al pulsar una tecla de movimiento y soltarla rápidamente, donde se apreciaba claramente que el movimiento no era instantáneo.

Ahora el problema no era la forma de enviar los datos, sino la latencia. ¿Cómo se resuelve? Pues prediciendo lo que el usuario va a hacer.

Client-side prediction (predicción en el cliente)

La predicción en el cliente se resolvió en dos partes: por un lado prediciendo el movimiento (implementado por John Carmack para Quakeworld en 1996), y por otro lado compensando la latencia (implementado por Yahn Bernier en Valve para Counter Strike).

Para predecir el movimiento el cliente ahora incluía una parte del código del servidor que funcionaba localmente en la máquina del propio jugador. De esta forma, cuando el usuario pulsaba una tecla veía inmediamente la acción en su pantalla. El problema de este planteamiento era: ¿cómo rectificar en caso de que el cliente y el servidor no estuvieran de acuerdo?

Una posible solución sería pensar que el cliente es el que mejor conoce al jugador. Por tanto, lo que diga el cliente tiene más veracidad que el servidor. Pero esta solución implicaría un posible uso malicioso para hacer trampas, ya que cada jugador podría engañar al servidor.

Predicción en el cliente simulando 500ms de latencia. La cámara en tercera persona muestra lo que vería el servidor. La cámara en primera persona es lo que ve el cliente.

En los FPS (First Person Shooter) el servidor debe ser el que mande en la partida, independientemente de si el cliente está prediciendo sus propias acciones. Así que no queda otra: el servidor SIEMPRE tiene la razón.

Pero claro, si aplicamos la corrección que nos acaba de mandar el servidor, estaremos “volviendo atrás en el tiempo” porque la acción ya ocurrió, así que aquí viene la otra parte del problema: la compensación de la latencia.

Para compensar la latencia la solución consistía en mantener un buffer circular de estados pasados del personaje y sus comandos. De esta forma, cuando el cliente recibiera la corrección desde el servidor, esta se descartaba si ya había ocurrido, o se repetían las acciones desde el estado más cercano seguro con las correcciones enviadas si era posible. El efecto conseguido era un contínuo “rebobinado y repetición” de los últimos frames, pero el usuario no era capaz de percibirlo por la rapidez del juego.

Un inteligente solución que, con sus matices, aún es usada hoy en día por franquicias como Call of Duty o Battlefield.