
En mayo de este año participé como jugador en . Observé que cuando el número de jugadores alcanza un cierto número, cada pocos minutos, parte de ellos "se desconectan". Afortunadamente para ustedes (pero no para mí), yo fui uno de esos jugadores que se desconectaban cada vez, incluso con una buena conexión. Lo tomé como un desafío personal y comencé a buscar las causas del problema. Después de tres semanas de depuración, pruebas y correcciones, finalmente se solucionó el error, pero este viaje no fue nada sencillo.
Los problemas en los juegos multijugador son muy difíciles de rastrear. Generalmente ocurren en condiciones muy específicas de parámetros de red y en situaciones de juego muy concretas (en este caso, con más de 200 jugadores). Y incluso cuando se logra reproducir el problema, no se puede depurar adecuadamente, porque insertar puntos de control detiene el juego, confunde los temporizadores y generalmente conduce a una desconexión por tiempo de espera. Pero gracias a la perseverancia y a una maravillosa herramienta llamada , logré averiguar qué estaba sucediendo.
En resumen: debido a un error y a una implementación incompleta de la simulación del estado de latencia, el cliente a veces se encontraba en una situación donde tenía que enviar en un solo ciclo un paquete de red que contenía las acciones de entrada del jugador para aproximadamente 400 entidades del juego (nosotros lo llamamos "megapaquete"). Después de eso, el servidor no solo debe recibir correctamente todas esas acciones de entrada, sino también enviarlas a todos los demás clientes. Si tienes 200 clientes, esto rápidamente se convierte en un problema. El canal hacia el servidor se congestiona rápidamente, lo que provoca pérdida de paquetes y un torrente de solicitudes de paquetes repetidos. La demora en las acciones de entrada provoca que aún más clientes comiencen a enviar megapaquetes, y su avalancha se vuelve aún más intensa. Los clientes afortunados logran recuperarse, los demás "se desconectan".

El problema era bastante fundamental, y me tomó 2 semanas resolverlo. Es bastante técnico, así que a continuación explicaré algunos detalles técnicos jugosos. Pero primero, necesitas saber que desde la versión 0.17.54, lanzada el 4 de junio, en condiciones de problemas temporales de conexión, el multijugador se volvió más estable y la ocultación de latencias es mucho menos problemática (menos retardos y teletransportes). Además, he cambiado la forma de ocultar las latencias en combate y espero que gracias a eso sean un poco más suaves.
Detalles técnicos del megapaquete multijugador
Simplificando, el multijugador en el juego funciona de la siguiente manera: todos los clientes simulan el estado del juego, recibiendo y enviando solo la entrada del jugador (denominada "acciones de entrada", Input Actions). La tarea principal del servidor es la transmisión Input Actions y el control de que todos los clientes realicen las mismas acciones en un mismo ciclo. Puedes leer más al respecto en el post .
Dado que el servidor debe tomar decisiones sobre qué acciones realizar, la acción del jugador sigue un camino aproximadamente así: acción del jugador -> cliente de juego -> red -> servidor -> red -> cliente de juego. Esto significa que cada acción del jugador se ejecuta solo después de realizar este recorrido de ida y vuelta a través de la red. Debido a esto, el juego podría parecer increíblemente lento, por lo que casi inmediatamente después de la aparición del multijugador en el juego se introdujo un mecanismo de ocultación de latencias. La ocultación de latencias simula la entrada del jugador sin tener en cuenta las acciones de otros jugadores y las decisiones del servidor.

En Factorio, hay un estado del juego Game State — este es el estado completo del mapa, del jugador, de las entidades y de todo lo demás. Se simula de manera determinista en todos los clientes basado en las acciones recibidas del servidor. El estado del juego es sagrado, y si alguna vez comienza a diferir del servidor o de cualquier otro cliente, se produce una desincronización.
Además Game State tenemos un estado de latencias Latency State. Contiene un pequeño subconjunto del estado principal. Latency State no es sagrado y simplemente representa una imagen de cómo se verá el estado del juego en el futuro basado en la entrada del jugador. Input Actions.
Para esto, almacenamos una copia de las acciones creadas Input Actions en la cola de latencias.

Es decir, al final del proceso, del lado del cliente la imagen se ve más o menos así:
- Aplicamos Input Actions todos los jugadores a Game State de la misma manera que estas acciones de entrada fueron recibidas del servidor.
- Eliminamos de la cola de retrasos todos los Input Actions, que, según los datos del servidor, ya se habían aplicado a Game State.
- Eliminamos Latency State y lo reiniciamos para que se vea exactamente como Game State.
- Aplicamos todas las acciones de la cola de retrasos a Latency State.
- Con base en los datos Game State y Latency State renderizamos el juego para el jugador.
Todo esto se repite en cada ciclo.
¿Demasiado complicado? No se relaje, esto aún no es todo. Para compensar la falta de fiabilidad de las conexiones a Internet, hemos creado dos mecanismos:
- Ciclos omitidos: cuando el servidor decide que Input Actions se llevarán a cabo en el ciclo del juego, si no ha recibido Input Actions de algún jugador (por ejemplo, debido a un aumento en la latencia), no esperará, sino que comunicará a ese cliente 'no he tenido en cuenta tus Input Actions, intentaré agregarlos en el siguiente ciclo'. Esto se hace para que, debido a problemas de conexión (o de computadora) de un jugador, la actualización del mapa no se retrase para todos los demás. Cabe mencionar que Input Actions no se ignoran, sino que simplemente se posponen.
- Retraso total de ida y vuelta: el servidor intenta suponer cuál es el retraso de la transmisión de datos de ida y vuelta entre el cliente y el servidor para cada cliente. Cada 5 segundos, discute con el cliente el nuevo retraso si es necesario (dependiendo de cómo se comportó la conexión en el pasado), y aumenta o reduce el retraso de la transmisión de datos de ida y vuelta en consecuencia.
Estos mecanismos en sí mismos son bastante simples, pero cuando se usan juntos (lo cual ocurre a menudo con problemas de conexión), la lógica del código se vuelve difícil de manejar y con muchos casos límite. Además, cuando entran en juego estos mecanismos, el servidor y la cola de retrasos deben implementar correctamente un particular Input Action llamada StopMovementInTheNextTick. Gracias a esto, ante problemas de conexión el personaje no se moverá solo (por ejemplo, hacia un tren).
Ahora necesito explicarles cómo funciona la selección de entidades. Uno de los tipos transmitidos Input Action — este es un cambio en el estado de selección de la entidad. Informa a todos sobre qué entidad ha señalado el jugador con el cursor. Como se puede entender, esta es una de las acciones de entrada más frecuentes enviadas por los clientes, por lo que, para ahorrar ancho de banda, la hemos optimizado para que ocupe el menor espacio posible. Se implementa de la siguiente manera: al seleccionar cada entidad, en lugar de guardar las coordenadas absolutas y de alta precisión del mapa, el juego guarda un desplazamiento relativo de baja precisión desde la selección anterior. Esto funciona bien porque la selección con el mouse generalmente ocurre muy cerca de la selección anterior. Por lo tanto, surgen dos requisitos importantes: Input Actions nunca se deben omitir y deben ejecutarse en el orden correcto. Estos requisitos se satisfacen para Game State. Pero dado que la tarea Estado de latencia es 'parecer lo suficientemente bueno' para el jugador, en el estado de latencias no se cumplen. Latency State no tiene en cuenta , relacionados con la omisión de ciclos y el cambio de latencias de ida y vuelta.
Ya puedes imaginar a dónde va todo esto. Por fin comenzamos a ver las causas del problema del megapaquete. La raíz del problema es que, al tomar la decisión sobre si se debe enviar la acción de cambio de selección, la lógica de selección de entidades se basa en Latency State, y este estado no siempre contiene información correcta. Por lo tanto, el megapaquete se genera aproximadamente de esta manera:
- El jugador tiene problemas de conexión.
- Entran en juego los mecanismos de omisión de ciclos y regulación de la latencia de ida y vuelta.
- La cola del estado de latencias no tiene en cuenta estos mecanismos. Esto lleva a que algunas acciones sean eliminadas prematuramente o ejecutadas en el orden incorrecto, lo que resulta en un incorrecto Latency State.
- El jugador supera el problema de conexión y, para ponerse al día con el servidor, simula hasta 400 ciclos.
- En cada ciclo se genera y se prepara para enviar al servidor una nueva acción de cambio de selección de entidad.
- El cliente envía al servidor un megapaquete de más de 400 cambios de selección de entidades (y otras acciones: el estado de disparo, caminar, etc. también sufrieron de este problema).
- El servidor recibe 400 acciones de entrada. Como no se le permite omitir ninguna acción de entrada, ordena a todos los clientes realizar esas acciones y las envía a través de la red.
La ironía es que el mecanismo diseñado para ahorrar ancho de banda, resulta que creaba enormes paquetes de red.
Resolvimos este problema corrigiendo todos los casos límite de actualización y soporte de la cola de retrasos. Aunque tomó bastante tiempo, al final valió la pena implementarlo correctamente en lugar de confiar en soluciones rápidas.
Fuente: habr.com
