En mayo de este año, participé como jugador en el
evento KatherineOfSky MMO . Noté que cuando el número de jugadores alcanza un cierto número, cada pocos minutos algunos de ellos "se caen". Afortunadamente para ti (pero no para mí), fui uno de esos jugadores que se desconectaba
todo el tiempo , incluso con una buena conexión. Tomé esto como un desafío personal y comencé a buscar las causas del problema. Después de tres semanas de depuración, prueba y reparación, el error finalmente se solucionó, pero este viaje no fue tan simple.
Los problemas de los juegos multijugador son muy difíciles de localizar. Por lo general, surgen en condiciones muy específicas de parámetros de red y en condiciones muy específicas del juego (en este caso, la presencia de más de 200 jugadores). E incluso cuando es posible reproducir el problema, no se puede depurar correctamente, porque la inserción de puntos de control detiene el juego, confunde los temporizadores y generalmente conduce a la terminación de la conexión debido a que excede el tiempo de espera. Pero gracias a la terquedad y la maravillosa herramienta llamada
torpe, logré entender lo que estaba sucediendo.
En resumen: debido a un error y una implementación incompleta de la simulación del estado de retraso, el cliente a veces se encuentra en una situación en la que tiene que enviar un paquete de red en un ciclo de reloj, que consiste en las acciones del jugador para seleccionar alrededor de 400 entidades de juego (lo llamamos un "megapaquete"). Después de eso, el servidor no solo debe recibir correctamente todas estas acciones de entrada, sino también enviarlas a todos los demás clientes. Si tiene 200 clientes, esto se convierte rápidamente en un problema. El canal hacia el servidor se obstruye rápidamente, lo que conduce a la pérdida de paquetes y a una cascada de paquetes solicitados nuevamente. Posponer acciones de entrada lleva al hecho de que incluso más clientes comienzan a enviar megapaquetes, y su avalancha se vuelve aún más fuerte. Los clientes exitosos logran recuperarse, todo lo demás "se cae".

El problema era bastante fundamental, y me tomó 2 semanas solucionarlo. Es bastante técnico, así que a continuación explicaré los detalles técnicos jugosos. Pero primero debe saber que desde la versión 0.17.54, lanzada el 4 de junio, ante problemas de conexión temporales, el modo multijugador se ha vuelto más estable, y la ocultación de retrasos es mucho menos problemática (menos frenado y teletransportación). Además, cambié la forma de ocultar los retrasos en el combate y espero que gracias a esto sean un poco más suaves.
Megapack multiusuario - Detalles técnicos
En términos simples, 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 (llamada "Acciones de entrada",
Acciones de entrada ). La tarea principal del servidor es transmitir
acciones de entrada y controlar que todos los clientes realicen las mismas acciones en un ciclo. Lea más sobre esto en el post
FFF-149 .
Dado que el servidor debe tomar decisiones sobre qué acciones realizar, las acciones del jugador se mueven a lo largo de este camino: acción del jugador -> cliente del juego -> red -> servidor -> red -> cliente del juego. Esto significa que la acción de cada jugador se realiza solo después de que realiza un recorrido de ida y vuelta a través de la red. Debido a esto, el juego parecería terriblemente lento, por lo que casi inmediatamente después de que apareciera el modo multijugador, se introdujo un mecanismo para ocultar los retrasos. Ocultar un retraso imita la aportación de un jugador sin tener en cuenta las acciones de otros jugadores y las decisiones del servidor.
Factorio tiene un estado de juego llamado
Game State : este es el estado completo del mapa, jugador, entidades y todo lo demás. Se simula determinísticamente en todos los clientes en función de las acciones recibidas del servidor. El estado del juego es sagrado, y si alguna vez comienza a diferir del servidor o cualquier otro cliente, se produce la desincronización.
Además de
Game State , tenemos un
estado de latencia. Contiene un pequeño subconjunto del estado fundamental.
El estado de latencia no
es sagrado y simplemente presenta una imagen de cómo se verá el estado del juego en el futuro en función de las
acciones de entrada introducidas por el jugador.
Para hacer esto, almacenamos una copia de las
acciones de entrada creadas en la cola de retraso.
Es decir, al final del proceso en el lado del cliente, la imagen se ve así:
- Aplique las acciones de entrada de todos los jugadores al estado del juego, ya que estas acciones de entrada se recibieron del servidor.
- Eliminamos de la cola de retrasos todas las acciones de entrada que, según el servidor, ya se han aplicado al estado del juego .
- Elimine el estado de latencia y reinícielo para que se vea exactamente igual al estado del juego .
- Aplique todas las acciones de la cola de retraso al estado de latencia .
- En base a los datos del estado del juego y el estado de latencia, presentamos el juego al jugador.
Todo esto se repite en cada medida.
Demasiado complicado? No te relajes, eso no es todo. Para compensar las conexiones de Internet poco confiables, creamos dos mecanismos:
- Ticks perdidos: cuando el servidor decide que las acciones de entrada se realizarán en el ritmo del juego, si no recibe las acciones de entrada de algunos jugadores (por ejemplo, debido al aumento de la demora), no esperará, pero le dirá a este cliente "No tuve en cuenta sus acciones de entrada , intentaré agregarlas a la siguiente medida ". Esto se hace para que, debido a problemas con la conexión (o con la computadora) de un jugador, la actualización del mapa no disminuya para todos los demás. Vale la pena señalar que las acciones de entrada no se ignoran, sino que simplemente se posponen.
- Retraso de ida y vuelta: el servidor está tratando de adivinar cuál es el retraso de ida y vuelta entre el cliente y el servidor para cada cliente. Cada 5 segundos, si es necesario, discute con el cliente un nuevo retraso (dependiendo de cómo se haya comportado la conexión en el pasado) y, en consecuencia, aumenta o disminuye el retraso en la transferencia de datos de un lado a otro.
Por sí mismos, estos mecanismos son bastante simples, pero cuando se usan juntos (lo que a menudo ocurre con problemas de conexión), la lógica del código se vuelve difícil de administrar y con un montón de casos límite. Además, cuando estos mecanismos entran en
juego , el servidor y la cola de retraso deben implementar correctamente una
acción de entrada especial llamada
StopMovementInTheNextTick . Debido a esto, con problemas con la conexión, el personaje no se ejecutará solo (por ejemplo, debajo del tren).
Ahora debe explicarle cómo funciona la selección de entidades. Uno de los tipos de
acción de
entrada aprobada es el cambio en el estado de la selección de entidad. Les dice a todos sobre qué entidad se cernía el jugador. Como puede comprender, esta es una de las acciones de entrada más frecuentes enviadas por los clientes, por lo que para ahorrar ancho de banda, la optimizamos para que ocupe el menor espacio posible. Esto se implementa de la siguiente manera: al elegir cada entidad, en lugar de preservar las coordenadas absolutas y de alta precisión del mapa, el juego conserva un desplazamiento relativo de baja corriente de la elección anterior. Esto funciona bien porque la selección del mouse generalmente ocurre muy cerca de la selección anterior. Debido a esto, surgen dos requisitos importantes: las
acciones de entrada nunca deben omitirse y deben realizarse en el orden correcto. Estos requisitos se cumplen para
Game State . Pero dado que la tarea del
estado de Latencia es "verse lo suficientemente bien" para el jugador, no está satisfecho con el estado de los retrasos.
El estado de latencia no tiene en cuenta
muchos casos límite asociados con saltos de ciclos y cambios en los retrasos de ida y vuelta.
Ya puedes adivinar a dónde va todo. Finalmente, comenzamos a ver las causas del problema del megapaquete. La raíz del problema es que al decidir si pasar la acción del cambio de selección, la lógica de selección de entidad se basa en el
estado de latencia , y este estado no siempre contiene la información correcta. Por lo tanto, se genera un megapaquete como este:
- El jugador tiene un problema de conexión.
- Los mecanismos para omitir relojes y regular el retraso de ida y vuelta entran en juego.
- Los retrasos del estado en cola no tienen en cuenta estos mecanismos. Esto hace que algunas acciones se eliminen prematuramente o se realicen en el orden incorrecto, lo que da como resultado un estado de latencia incorrecto.
- El jugador tiene un problema con la conexión y él, para ponerse al día con el servidor, simula hasta 400 ciclos de reloj.
- En cada ciclo de reloj, se genera una nueva acción, que cambia la selección de una entidad, y se prepara para enviarla al servidor.
- El cliente envía al servidor un megapaquete de más de 400 cambios en la elección de entidades (y con otras acciones: el estado de disparar, caminar, etc., también sufrió este problema).
- El servidor recibe 400 acciones de entrada. Como no se le permite omitir una sola acción de entrada, ordena a todos los clientes que realicen estas acciones y las envía a través de la red.
La ironía es que un mecanismo diseñado para ahorrar ancho de banda del canal creó grandes paquetes de red como resultado.
Resolvimos este problema corrigiendo todos los casos límite de actualización y apoyando la cola de demoras. Aunque tomó bastante tiempo, al final valió la pena implementar todo correctamente y no confiar en hacks rápidos.