Sobre el modelo de red en juegos para principiantes

Sobre el modelo de red en juegos para principiantes
Durante las últimas dos semanas he estado trabajando en el motor de red de mi juego. Antes de eso, no sabía nada sobre las redes en los juegos, así que leí muchos artículos e hice muchos experimentos para comprender todos los conceptos y poder escribir mi propio motor de redes.

En esta guía, me gustaría compartir contigo los diversos conceptos que necesitas aprender antes de escribir tu propio motor de juego, así como los mejores recursos y artículos para aprenderlos.

En general, existen dos tipos principales de arquitecturas de red: peer-to-peer y cliente-servidor. En una arquitectura peer-to-peer (p2p), los datos se transfieren entre cualquier par de jugadores conectados, mientras que en una arquitectura cliente-servidor, los datos se transfieren solo entre los jugadores y el servidor.

Aunque la arquitectura peer-to-peer todavía se usa en algunos juegos, cliente-servidor es el estándar: es más fácil de implementar, requiere un ancho de canal más pequeño y facilita la protección contra trampas. Por lo tanto, en esta guía nos centraremos en la arquitectura cliente-servidor.

En particular, estamos más interesados ​​en servidores autoritarios: en tales sistemas, el servidor siempre tiene la razón. Por ejemplo, si el jugador cree que está en (10, 5) y el servidor le dice que está en (5, 3), entonces el cliente debe reemplazar su posición con la que informa el servidor, no al revés. El uso de servidores autorizados facilita el reconocimiento de los tramposos.

Hay tres componentes principales en los sistemas de red de juegos:

  • Protocolo de transporte: cómo se transfieren los datos entre los clientes y el servidor.
  • Protocolo de aplicación: qué se transmite de los clientes al servidor y del servidor a los clientes, y en qué formato.
  • Lógica de aplicación: cómo se utilizan los datos transmitidos para actualizar el estado de los clientes y el servidor.

Es muy importante comprender el papel de cada parte y las dificultades asociadas con ellas.

Protocolo de transporte

El primer paso es elegir un protocolo para transportar datos entre el servidor y los clientes. Hay dos protocolos de Internet para esto: TCP и UDP. Pero puede crear su propio protocolo de transporte basado en uno de ellos o usar una biblioteca que los use.

Comparación de TCP y UDP

Tanto TCP como UDP se basan en IP. IP permite que un paquete se transmita de una fuente a un receptor, pero no garantiza que el paquete enviado llegará tarde o temprano al receptor, que llegará al menos una vez y que la secuencia de paquetes llegará en el orden correcto. Además, un paquete solo puede contener un tamaño de datos limitado, dado por el valor MTU.

UDP es solo una capa delgada sobre IP. Por lo tanto, tiene las mismas limitaciones. En contraste, TCP tiene muchas características. Proporciona una conexión ordenada confiable entre dos nodos con verificación de errores. Por lo tanto, TCP es muy conveniente y se usa en muchos otros protocolos, por ejemplo, en HTTP, FTP и SMTP. Pero todas estas características tienen un precio: demora.

Para comprender por qué estas funciones pueden causar latencia, debemos comprender cómo funciona TCP. Cuando el host emisor transmite un paquete al host receptor, espera recibir un acuse de recibo (ACK). Si después de un cierto tiempo no lo recibe (porque se perdió el paquete o la confirmación, o por alguna otra razón), entonces envía el paquete nuevamente. Además, TCP garantiza que los paquetes se reciban en el orden correcto, por lo que hasta que se reciba un paquete perdido, todos los demás paquetes no se pueden procesar, incluso si ya los ha recibido el nodo receptor.

Pero como probablemente entiendas, la latencia en los juegos multijugador es muy importante, especialmente en géneros tan activos como los FPS. Es por eso que muchos juegos usan UDP con su propio protocolo.

Un protocolo nativo basado en UDP puede ser más eficiente que TCP por varias razones. Por ejemplo, puede marcar algunos paquetes como confiables y otros como no confiables. Por lo tanto, no le importa si el paquete no confiable ha llegado al destinatario. O puede procesar múltiples flujos de datos para que un paquete perdido en un flujo no ralentice otros flujos. Por ejemplo, puede haber un hilo para la entrada del jugador y otro hilo para los mensajes de chat. Si se pierde un mensaje de chat que no es información urgente, no ralentizará la entrada que es urgente. O un protocolo propietario podría implementar la confiabilidad de manera diferente a TCP para ser más eficiente en un entorno de videojuegos.

Entonces, si TCP apesta, ¿entonces vamos a construir nuestro propio protocolo de transporte basado en UDP?

Todo es un poco más complicado. Aunque TCP es casi subóptimo para los sistemas de red de juegos, puede funcionar bastante bien para su juego específico y ahorrarle un tiempo valioso. Por ejemplo, la latencia puede no ser un problema para un juego por turnos o un juego que solo se puede jugar en redes LAN, donde la latencia y la pérdida de paquetes son mucho menores que en Internet.

Muchos juegos exitosos, incluidos World of Warcraft, Minecraft y Terraria, usan TCP. Sin embargo, la mayoría de los FPS utilizan sus propios protocolos basados ​​en UDP, por lo que hablaremos más sobre ellos a continuación.

Si elige usar TCP, asegúrese de que esté deshabilitado algoritmo de Nagle, porque almacena en búfer los paquetes antes de enviarlos, lo que significa que aumenta el retraso.

Para obtener más información sobre las diferencias entre UDP y TCP en el contexto de los juegos multijugador, consulte el artículo de Glenn Fiedler. UDP frente a TCP.

Protocolo propietario

¿Quiere crear su propio protocolo de transporte pero no sabe por dónde empezar? Estás de suerte, porque Glenn Fiedler escribió dos artículos increíbles al respecto. Encontrarás muchas ideas inteligentes en ellos.

Primer artículo Redes para programadores de juegos 2008, más fácil que el segundo Creación de un protocolo de red de juegos 2016. Te recomiendo que empieces con el más antiguo.

Tenga en cuenta que Glenn Fiedler es un gran defensor del uso de su propio protocolo basado en UDP. Y después de leer sus artículos, probablemente adoptará su opinión de que TCP tiene serios inconvenientes en los videojuegos, y querrá implementar su propio protocolo.

Pero si es nuevo en las redes, hágase un favor y use TCP o una biblioteca. Para implementar con éxito su propio protocolo de transporte, debe aprender mucho de antemano.

Bibliotecas de red

Si necesita algo más eficiente que TCP, pero no quiere molestarse en implementar su propio protocolo y entrar en muchos detalles, puede usar la biblioteca de red. Hay muchos de ellos:

No los he probado todos, pero prefiero ENet porque es fácil de usar y confiable. Además, tiene una documentación clara y un tutorial para principiantes.

Conclusión del protocolo de transporte

Para resumir, hay dos protocolos de transporte principales: TCP y UDP. TCP tiene muchas características útiles: confiabilidad, conservación del orden de los paquetes, detección de errores. UDP no tiene todo eso, pero TCP, por su propia naturaleza, tiene una latencia alta que es inaceptable para algunos juegos. Es decir, para garantizar una baja latencia, puedes crear tu propio protocolo basado en UDP o usar una biblioteca que implemente el protocolo de transporte en UDP y esté adaptada para videojuegos multijugador.

La elección entre TCP, UDP y la biblioteca depende de varios factores. Primero, por las necesidades del juego: ¿necesita baja latencia? En segundo lugar, de los requisitos del protocolo de aplicación: ¿necesita un protocolo fiable? Como veremos en la siguiente parte, es posible crear un protocolo de aplicación para el cual un protocolo no confiable es bastante adecuado. Finalmente, también debe considerar la experiencia del desarrollador del motor de red.

Tengo dos consejos:

  • Abstraiga el protocolo de transporte tanto como sea posible del resto de la aplicación para que pueda reemplazarse fácilmente sin tener que volver a escribir todo el código.
  • No sobreoptimice. Si no es un experto en redes y no está seguro de si necesita su propio protocolo de transporte basado en UDP, puede comenzar con TCP o una biblioteca que proporcione confiabilidad y luego probar y medir el rendimiento. Si tiene problemas y está seguro de que se trata de un protocolo de transporte, puede que sea el momento de crear su propio protocolo de transporte.

Al final de esta parte, le recomiendo que lea Introducción a la programación de juegos multijugador Brian Hook, que cubre muchos de los temas discutidos aquí.

Protocolo de aplicación

Ahora que podemos intercambiar datos entre los clientes y el servidor, debemos decidir qué datos transferir y en qué formato.

El esquema clásico es que los clientes envían entradas o acciones al servidor, y el servidor envía el estado actual del juego a los clientes.

El servidor no envía el estado completo, sino el filtrado con las entidades que están cerca del jugador. Lo hace por tres razones. Primero, el estado total puede ser demasiado grande para transmitir a alta frecuencia. En segundo lugar, los clientes están interesados ​​principalmente en datos visuales y de audio, porque la mayor parte de la lógica del juego se simula en el servidor del juego. En tercer lugar, en algunos juegos el jugador no necesita saber ciertos datos, como la posición del enemigo al otro lado del mapa, porque de lo contrario puede olfatear paquetes y saber exactamente dónde moverse para matarlo.

Publicación por entregas

El primer paso es convertir los datos que queremos enviar (entrada o estado del juego) a un formato apto para la transmisión. Este proceso se llama publicación por entregas.

Inmediatamente me viene a la mente la idea de utilizar un formato legible por humanos, como JSON o XML. Pero esto será completamente ineficiente y ocupará la mayor parte del canal para nada.

En su lugar, se recomienda utilizar el formato binario, que es mucho más compacto. Es decir, los paquetes solo contendrán unos pocos bytes. Aquí hay que tener en cuenta el problema orden de bytes, que puede diferir en diferentes equipos.

Para serializar datos, puede usar una biblioteca, por ejemplo:

Solo asegúrese de que la biblioteca cree archivos portátiles y se encargue de endianness.

Una solución alternativa sería implementarlo usted mismo, no es tan difícil, especialmente si está utilizando un enfoque centrado en datos en su código. Además, te permitirá realizar optimizaciones que no siempre son posibles al usar la librería.

Glenn Fiedler ha escrito dos artículos sobre serialización: Paquetes de lectura y escritura и Estrategias de serialización.

compresión

La cantidad de datos transferidos entre los clientes y el servidor está limitada por el ancho de banda del canal. La compresión de datos le permitirá transferir más datos en cada instantánea, aumentar la frecuencia de actualización o simplemente reducir los requisitos de ancho de banda.

Embalaje de bits

La primera técnica es el empaquetamiento de bits. Consiste en utilizar exactamente el número de bits necesarios para describir el valor deseado. Por ejemplo, si tiene una enumeración que puede tener 16 valores diferentes, en lugar de un byte completo (8 bits), puede usar solo 4 bits.

Glenn Fiedler explica cómo implementar esto en la segunda parte del artículo. Paquetes de lectura y escritura.

El empaquetamiento de bits funciona particularmente bien con la discretización, que será el tema de la siguiente sección.

Muestreo

Muestreo es una técnica de compresión con pérdida que utiliza solo un subconjunto de valores posibles para codificar un valor. La forma más fácil de implementar la discretización es redondeando números de punto flotante.

Glenn Fiedler (¡otra vez!) muestra cómo aplicar la discretización en la práctica en su artículo Compresión de instantáneas.

Algoritmos de compresión

La siguiente técnica serán los algoritmos de compresión sin pérdidas.

Aquí, en mi opinión, están los tres algoritmos más interesantes que necesitas saber:

  • Codificación Huffman con código precomputado, que es extremadamente rápido y puede producir buenos resultados. Se utilizó para comprimir paquetes en el motor de red Quake3.
  • zlib es un algoritmo de compresión de propósito general que nunca aumenta la cantidad de datos. Como puedes ver aquí, se ha utilizado en una variedad de aplicaciones. Para actualizar estados, puede ser redundante. Pero puede resultar útil si necesita enviar activos, textos extensos o terreno a los clientes desde el servidor.
  • Copia de tiradas es probablemente el algoritmo de compresión más simple, pero es muy eficiente para ciertos tipos de datos y se puede usar como un paso de preprocesamiento antes de zlib. Es especialmente adecuado para comprimir terrenos formados por mosaicos o vóxeles en los que se repiten muchos elementos vecinos.

compresión delta

La última técnica de compresión es la compresión delta. Se encuentra en el hecho de que solo se transmiten las diferencias entre el estado actual del juego y el último estado recibido por el cliente.

Se utilizó por primera vez en el motor de red Quake3. Aquí hay dos artículos que explican cómo usarlo:

Glenn Fiedler también lo utilizó en la segunda parte de su artículo. Compresión de instantáneas.

Cifrado

Además, es posible que deba cifrar la transmisión de información entre los clientes y el servidor. Hay varias razones para esto:

  • Privacidad/Confidencialidad: los mensajes solo pueden ser leídos por el destinatario y ningún otro rastreador de red podrá leerlos.
  • autenticación: una persona que quiere jugar el papel de un jugador debe conocer su clave.
  • prevención de trampas: será mucho más difícil para los jugadores maliciosos crear sus propios paquetes de trampas, tendrán que replicar el esquema de encriptación y encontrar la clave (que cambia en cada conexión).

Recomiendo encarecidamente usar una biblioteca para esto. sugiero usar libsodio, porque es especialmente simple y tiene excelentes tutoriales. Particularmente interesante es el tutorial sobre intercambio de llaves, que le permite generar nuevas claves en cada nueva conexión.

Protocolo de aplicación: Conclusión

Esto concluye el protocolo de aplicación. Creo que la compresión es completamente opcional y la decisión de usarla depende solo del juego y del ancho de banda requerido. El cifrado, en mi opinión, es obligatorio, pero en el primer prototipo puedes prescindir de él.

Lógica de aplicación

Ahora podemos actualizar el estado en el cliente, pero es posible que tengamos problemas de latencia. El jugador, después de hacer una entrada, debe esperar una actualización del estado del juego del servidor para ver qué efecto ha tenido en el mundo.

Además, entre dos actualizaciones de estado, el mundo es completamente estático. Si la tasa de actualización del estado es baja, los movimientos serán muy bruscos.

Hay varias técnicas para mitigar el impacto de este problema, y ​​las discutiré en la siguiente sección.

Técnicas de suavizado de retardo

Todas las técnicas descritas en esta sección se analizan en detalle en la serie. Multijugador de ritmo rápido Gabriel Gambeta. Recomiendo leer esta excelente serie de artículos. También incluye una demostración interactiva para ver cómo funcionan estas técnicas en la práctica.

La primera técnica es aplicar el resultado de entrada directamente sin esperar una respuesta del servidor. Se llama predicción del lado del cliente. Sin embargo, cuando el cliente recibe una actualización del servidor, debe verificar que su predicción fue correcta. Si este no es el caso, entonces solo necesita cambiar su estado de acuerdo con lo que recibió del servidor, porque el servidor es autoritario. Esta técnica se utilizó por primera vez en Quake. Puedes leer más sobre esto en el artículo. Revisión del código del motor de Quake Fabien Sanglars [traducción sobre Habré].

El segundo conjunto de técnicas se utiliza para suavizar el movimiento de otras entidades entre dos actualizaciones de estado. Hay dos formas de resolver este problema: la interpolación y la extrapolación. En el caso de la interpolación, se toman los dos últimos estados y se muestra la transición de uno a otro. Su desventaja es que provoca una pequeña fracción del retraso, porque el cliente siempre ve lo que pasó en el pasado. La extrapolación se trata de predecir dónde deberían estar ahora las entidades en función del último estado recibido por el cliente. Su desventaja es que si la entidad cambia completamente la dirección del movimiento, habrá un gran error entre el pronóstico y la posición real.

La última técnica más avanzada, útil solo en FPS, es compensación de retraso. Cuando se utiliza la compensación de retraso, el servidor tiene en cuenta los retrasos del cliente cuando dispara al objetivo. Por ejemplo, si un jugador realizó un tiro en la cabeza en su pantalla, pero en realidad su objetivo estaba en una ubicación diferente debido a la demora, sería injusto negarle al jugador el derecho a matar debido a la demora. Entonces, el servidor retrocede el tiempo hasta cuando el jugador disparó para simular lo que el jugador vio en su pantalla y verificar si hay una colisión entre su disparo y el objetivo.

Glenn Fiedler (¡como siempre!) escribió un artículo en 2004 Física de redes (2004), en el que sentó las bases para la sincronización de simulaciones físicas entre el servidor y el cliente. En 2014 escribió una nueva serie de artículos. física de redes, en el que describió otras técnicas para sincronizar simulaciones de física.

También hay dos artículos en la wiki de Valve, Fuente de redes multijugador и Métodos de compensación de latencia en el diseño y optimización de protocolos cliente / servidor en el juego ocuparse de la compensación por retraso.

Prevención de trampas

Hay dos técnicas principales de prevención de trampas.

Primero, dificultar que los tramposos envíen paquetes maliciosos. Como se mencionó anteriormente, una buena forma de implementarlo es el cifrado.

En segundo lugar, el servidor autorizado solo debe recibir comandos/entradas/acciones. El cliente no debería poder cambiar el estado en el servidor más que enviando una entrada. Luego, el servidor, cada vez que recibe una entrada, debe verificar su validez antes de aplicarla.

Lógica de aplicación: Conclusión

Te recomiendo que implementes una forma de simular latencias altas y frecuencias de actualización bajas para que puedas probar el comportamiento de tu juego en malas condiciones, incluso cuando el cliente y el servidor se ejecutan en la misma computadora. Esto simplifica enormemente la implementación de técnicas de suavizado de retardo.

Otros recursos útiles

Si desea explorar otros recursos de modelos de red, puede encontrarlos aquí:

Fuente: habr.com

Añadir un comentario