Reconocimiento de fiabilidad utilizando UDP

16

Tengo una pregunta sobre UDP. Por contexto, estoy trabajando en un juego de acción en tiempo real.

He leído bastante sobre las diferencias entre UDP y TCP y creo que las entiendo bastante bien, pero hay una pieza que nunca se ha sentido correcta, y es la fiabilidad y los reconocimientos específicos . Entiendo que UDP no ofrece confiabilidad por defecto (es decir, los paquetes se pueden descartar o llegar fuera de servicio). Cuando se requiere cierta confiabilidad, la solución que he visto (lo que tiene sentido conceptualmente) es usar confirmaciones (es decir, el servidor envía un paquete al cliente, y cuando el cliente recibe ese mensaje, envía una confirmación al servidor) .

¿Qué sucede cuando se descarta el reconocimiento?

En el ejemplo anterior (un servidor que envía un paquete a un cliente), el servidor maneja la pérdida potencial de paquetes reenviando paquetes en cada trama hasta que se reciban los acuses de recibo de esos paquetes. Aún podría encontrarse con problemas de ancho de banda o mensajes fuera de servicio, pero puramente desde una perspectiva de pérdida de paquetes, el servidor está cubierto.

Sin embargo, si el cliente envía un acuse de recibo que nunca llega, el servidor no tendría más remedio que dejar de enviar ese mensaje, lo que podría interrumpir el juego si se requiere la información contenida en ese paquete. Podría adoptar un enfoque similar al servidor (es decir, ¿seguir enviando acuses de recibo hasta que reciba un reconocimiento por el reconocimiento?), Pero ese enfoque lo obligaría a ir y venir para siempre (ya que necesitaría un reconocimiento por el reconocimiento por el reconocimiento) y así).

Siento que mi lógica básica es correcta aquí, lo que me deja con dos opciones.

  1. Envíe un solo paquete de reconocimiento y espere lo mejor.
  2. Envíe un puñado de paquetes de reconocimiento (quizás 3-4) y espere lo mejor, suponiendo que no todos se descartarán.

¿Hay una respuesta a este problema? ¿Estoy fundamentalmente malentendido algo? ¿Hay alguna garantía de usar UDP que desconozco? Me siento reacio a avanzar con demasiado código de red hasta que me sienta cómodo de que mi lógica es sólida.

Grimelios
fuente
11
Quizás te estás perdiendo una idea de "tiempos de espera" y "reintentos".
Kromster dice que apoya a Mónica el
Yo podría estar seguro. ¿Estás sugiriendo que mi lógica es correcta y, para no parecer demasiado negativa, pero mientras programo la red, no puedo asumir garantías sobre prácticamente ninguna información en red? Durante el transcurso de un juego en tiempo real, hay un montón de información potencialmente descartada, lo cual está bien, pero solo quiero asegurarme de que entiendo el problema.
Grimelios
10
No hay garantías en absoluto. Correcto. Nunca incluyas "esperanza" en tus algoritmos. Deben manejar CUALQUIER combinación desafortunada. PD: Simplemente cambiamos a TCP en nuestro RTS, donde se ocupa de todo, ya que necesitamos una comunicación confiable (para la simulación de bloqueos).
Kromster dice que apoya a Monica el
55
Use TCP cuando se necesite confiabilidad, use UDP cuando no importe. Por ejemplo, las coordenadas del jugador se envían en mi juego a través de UDP. Uso interpolación y suavizado para suavizar los paquetes que faltan. Funciona de maravilla. Las cosas que realmente necesitan ser confiables pero que pueden ser un poco más lentas se envían a través de TCP. Si tiene un estado en el que el estado más nuevo invalida el estado anterior, UDP es una buena opción, ya que no importa cuándo se cayó algo intermedio 8e.g. posición de jugador).
Polygnome
Esta no es una respuesta directa a su pregunta, pero le recomiendo que solo requiera un reconocimiento en un juego en tiempo real cuando sea absolutamente necesario (por ejemplo, en la conexión inicial). Es mucho más simple (y robusto) diseñar tanto el cliente como el servidor para que "trabajen con lo que tienen" hasta que puedan obtener un nuevo paquete en un sistema sin estado si es posible. Quake 3 hizo esto increíblemente bien con un sistema basado en instantáneas . Además, las bibliotecas como Enet pueden enviar solo ciertos paquetes de manera confiable, para aquellos casos en los que realmente lo necesita
jrh

Respuestas:

32

Esta es una forma del problema de los dos generales , y tiene razón: no hay suficientes reintentos para garantizar perfectamente la recepción.

En la práctica, en los juegos, generalmente hay un horizonte temporal más allá del cual la información realmente no importa, aunque técnicamente llegue de manera confiable. Como descubrir que tenía un tiro en la cabeza perfecto alineado hace 2 segundos, es demasiado tarde para que el jugador use esa información ahora.

Si su pérdida de paquetes es tan alta que no puede obtener de forma rutinaria la información necesaria dentro de una ventana de reacción ajustada, entonces para un juego en tiempo real, sería mejor patear al jugador e intentar encontrar una mejor coincidencia para ellos en otro lugar, en lugar de continúe intentando enviar el paquete para emular una conexión confiable.

Debido a esto, algunos sistemas de replicación de juegos omiten el reconocimiento y los reintentos por completo y optan por enviar spam a la actualización más reciente con la mayor frecuencia posible. Si uno se cae o llega tarde, lástima, sáltelo, tome el siguiente y continúe, confiando en los sistemas de predicción e interpolación para suavizar la brecha y minimizar el hipo visible para el jugador.

De repente quiero comenzar a llamar a esto "Simba Replication" por cómo ignora los problemas en el pasado y trata de vivir en el momento presente. ;)

Rafiki estableciendo algo reductio ad absurdum sobre esa filosofía de vida

Una solución híbrida es correr hacia adelante enviando la nueva actualización Y (dado que las actualizaciones de estado del juego a menudo pueden ser bastante pequeñas / compresibles ) también empacar en la última actualización, y tal vez la anterior ... Entonces, en caso de que el cliente las perdiera , no tiene que esperar un tiempo completo de ida y vuelta para averiguarlo y solucionarlo. La mayoría de las veces el cliente ya vio esto, por lo que hay datos redundantes de esta manera, pero la latencia para corregir un mensaje perdido es menor. Las actualizaciones del cliente pueden incluir el número de índice de la actualización consecutiva más reciente que han visto, por lo que puede ser mínimamente conservador con la cantidad de actualizaciones antiguas que incluye en el próximo paquete de actualizaciones.

También podría implementar un sistema de dos niveles como otro tipo de híbrido, donde el estado de corta duración se replica de manera poco confiable y el estado a largo plazo se sincroniza de manera confiable, utilizando TCP o su propia implementación de confiabilidad con un alto reintento contar. Sin embargo, esto se vuelve más complejo de administrar, porque tiene dos sistemas de mensajería que mantener y las dos instantáneas pueden estar fuera de sincronización entre sí, agregando una clase completamente nueva de caso límite.

DMGregory
fuente
1
+1, bien escrito. Solo destacaría que esto es más relevante para los juegos de acción / tiempo real. Los juegos TBS y RTS (y algunos eventos de juegos de acción) tienen una visión diferente del "horizonte temporal más allá del cual la información realmente no importa".
Kromster dice que apoya a Mónica el
3
Sí, para un juego por turnos, me imagino que uno usaría TCP en lugar de tratar de rodar la propia capa de confiabilidad sobre UDP. ;) Todavía clasificaría el micro en un RTS como el tipo de juego con un horizonte de tiempo exacto: ese enfoque híbrido puede funcionar bien allí, donde tiene actualizaciones de baja latencia para el calor del momento, así como un red de seguridad para manejar retroactivamente eventos críticos perdidos como el gasto de recursos.
DMGregory
Eso es extremadamente útil y valida mi preocupación inicial. Muchas gracias.
Grimelios
2
También podría ser útil mencionar Forward Error Correction. Diseñe su protocolo de manera que el receptor pueda determinar independientemente que un paquete se descartó cuando se recibe el siguiente paquete, agregando algunos datos adicionales para suavizar la interpolación requerida. Esto puede ser útil porque a menudo los paquetes UDP no están llenos de todos modos, y simplemente envía paquetes más pequeños con más frecuencia para mantener baja la latencia. Agregar algunos bytes adicionales no dañará la latencia, y el ancho de banda no es un problema en estos casos.
MSalters
@MSalters Diría que vale la pena explicarlo en su propia respuesta, si estás preparado para ello. Votaría eso. :)
DMGregory
9

El enfoque que utiliza TCP es que el remitente seguirá reenviando el paquete hasta que reciba un acuse de recibo. El receptor ignorará los paquetes duplicados, pero aún enviará acuses de recibo por ellos. El remitente ignorará los acuses de recibo duplicados.

Si se pierde un paquete, el remitente lo reenvía, como ya sabe.
Si se pierde un acuse de recibo, el remitente reenvía el paquete original, lo que hace que el receptor reenvíe el acuse de recibo.

Si no se recibe un acuse de recibo dentro de un cierto tiempo (quizás 60 segundos o 20 reintentos), se considera que el jugador está desconectado del juego. Usted debe aplicar algún tipo de regla de tiempo de espera, o de otro modo un jugador que desenchufa el cable de red, no podrá utilizar los recursos en el servidor para siempre.

usuario253751
fuente
Una característica esencial de TCP es que el remitente no necesita preocuparse por si se reconoció algún paquete en particular , sino que debe preocuparse principalmente por la "marca de límite superior" y cuánto tiempo han estado pendientes los paquetes sin que la marca de límite superior se haya movido.
supercat
1
@ Supercat No diría que es esencial; más como una optimización.
user253751
En cuanto a lo que está entre paréntesis (enviar ACK para los paquetes que ya tienes), creo que deberías enfatizarlo en lugar de ponerlo entre paréntesis. Parece estar ausente de la comprensión del OP (o al menos su descripción).
Angew ya no está orgulloso de SO
@Angew hecho ahora.
user253751
6

Si desea reinventar TCP, tiene sentido mirar primero TCP , que se ocupa del problema exacto que describe (parte de la solución es usar valores definidos por el usuario para intentos de reintento y tiempos de espera).

Las soluciones que usan 2 canales, un canal TCP (para comunicación confiable) y un canal UDP (para comunicación de baja latencia) no son infrecuentes.

Algunas soluciones detectan cuando a un cliente le falta información durante demasiado tiempo e inician una resincronización, que puede usar UDP o TCP.

Otro enfoque común es diseñar la comunicación de manera que no se base en absoluto en los reconocimientos, pero eso está fuera del alcance de la pregunta.

Peter
fuente
3

En un RTS, realmente no puede usar un protocolo como TCP, y tampoco puede hacer que UDP sea confiable. Si lo intentas, el juego se congelará cada vez que haya una conexión de red.

En su lugar, diseña el protocolo para que los paquetes perdidos no importen demasiado.

La versión corta es que no te importa dónde estuvieron los otros jugadores el último cuadro, siempre y cuando sepas dónde están ahora . La versión larga es más complicada.

La pregunta entonces es, ¿qué haces cuando un paquete se pierde? Y la respuesta es ... supongo. El jugador probablemente se está moviendo en línea recta, ¿verdad? Simplemente muévalos un paso más allá en esa línea. ... Excepto que ningún jugador de RTS se mueve en línea recta. Y luego está la detección de colisiones.

Esto es duro. Muchos juegos se equivocan. Se puede argumentar que no hay una respuesta correcta a esto, solo varios errores que se pueden cambiar.

La razón por la que estos juegos funcionan bastante bien no es solo porque han pensado mucho sobre estos problemas, sino también porque Internet se ha vuelto bastante confiable. Casi todos los paquetes UDP llegan a su destino de manera oportuna. (A menos que haya un problema permanente como un firewall)

Stig Hemmer
fuente
Warcraft 3 usa TCP.
fsp