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 para mi juego. Antes de esto, no sabía nada sobre tecnologías de red en los juegos, así que leí muchos artículos y realicé numerosos experimentos para comprender todos los conceptos y poder escribir mi propio motor de red.

En esta guía, me gustaría compartir con ustedes los diversos conceptos que deben estudiar antes de escribir su propio motor de juego, así como los mejores recursos y artículos para su aprendizaje.

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

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

En particular, nos interesan los servidores autoritarios: en dichos sistemas, el servidor siempre tiene la última palabra. Por ejemplo, si un jugador piensa que se encuentra en las coordenadas (10, 5), y el servidor le dice que está en (5, 3), el cliente debe reemplazar su posición por la que le indica el servidor, y no al revés. El uso de servidores autoritarios simplifica el reconocimiento de tramposos.

En los sistemas de red de juegos, hay tres componentes principales:

  • Protocolo de transporte: cómo se transmiten 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 la 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.

Protocolo de transporte

El primer paso es elegir un protocolo para transportar datos entre el servidor y los clientes. Existen dos protocolos de Internet para esto: TCP y UDP. Pero también puedes crear tu propio protocolo de transporte basado en uno de ellos o utilizar una biblioteca que los implemente.

Comparación de TCP y UDP

Tanto TCP como UDP se basan en IPLa IP permite enviar paquetes desde el origen al destinatario, pero no garantiza que el paquete enviado llegue al destinatario en algún momento, que lo reciba al menos una vez ni que la secuencia de paquetes llegue en el orden correcto. Además, un paquete puede contener solo un tamaño limitado de datos, determinado por el tamaño. MTU.

UDP es solo una capa delgada sobre IP. Por lo tanto, tiene las mismas limitaciones. A diferencia de él, TCP tiene muchas características. Proporciona una conexión confiable y ordenada entre dos nodos con verificación de errores. Por lo tanto, TCP es muy conveniente y se utiliza en muchos otros protocolos, como en. . La mejora del rendimiento de nginx se debe al uso de una arquitectura asíncrona basada en eventos, a diferencia del modelo multihilo. Esto, junto con un alto paralelismo, permite a nginx manejar solicitudes con un bajo consumo de memoria. Esto hace que Nginx sea una excelente solución para servidores web, tanto para grandes como para pequeños volúmenes de tráfico. nginx es una solución madura y completamente documentada, que permite configurar fácilmente la configuración según tus necesidades. Los desarrolladores también utilizan nginx como proxy inverso para, FTP y SMTPPero todas estas funciones tienen su costo: la latencia..

Para entender por qué estas funciones pueden causar latencia, debemos entender cómo funciona TCP. Cuando el nodo emisor envía un paquete al nodo receptor, espera recibir una confirmación (ACK). Si después de un tiempo determinado no la recibe (porque el paquete o la confirmación se perdieron, o por alguna otra razón), vuelve a enviar el paquete. Además, TCP garantiza la recepción de paquetes en el orden correcto, por lo que mientras no se reciba el paquete perdido, todos los demás paquetes no pueden ser procesados, incluso si ya han sido recibidos por el nodo receptor.

Pero, como seguramente entenderás, la latencia en los juegos multijugador es muy importante, especialmente en géneros tan activos como los FPS. Por eso, muchos juegos utilizan UDP con su propio protocolo.

Un protocolo propio basado en UDP puede ser más eficiente que TCP por diversas razones. Por ejemplo, puede marcar algunos paquetes como confiables y otros como no confiables. Por lo tanto, no le preocupa si el paquete no confiable llega al destinatario. O puede manejar múltiples flujos de datos para que un paquete perdido en un flujo no retrase los demás flujos. Por ejemplo, puede haber un flujo para la entrada del jugador y otro flujo para mensajes de chat. Si se pierde un mensaje de chat, que no es un dato urgente, no retrasará la activación de la entrada, que sí es urgente. O el protocolo propio puede implementar la fiabilidad de manera diferente a TCP, para ser más eficiente en condiciones de videojuegos.

Entonces, si TCP es tan malo, ¿vamos a crear nuestro propio protocolo de transporte basado en UDP?

La situación es un poco más complicada. Aunque TCP es casi subóptimo para sistemas de red de juegos, puede funcionar bastante bien en tu juego específico y ahorrarte 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, incluyendo World of Warcraft, Minecraft y Terraria, utilizan TCP. Sin embargo, en la mayoría de los FPS se aplican protocolos propios basados en UDP, así que a continuación hablaremos de ellos en más detalle.

Si decides usar TCP, asegúrate de que está desactivado el algoritmo de Nagle, porque este almacena paquetes antes de enviarlos, lo que aumenta la latencia.

Para conocer más sobre las diferencias entre UDP y TCP en el contexto de los juegos multijugador, puedes leer el artículo de Glenn Fidler UDP vs. TCP.

Protocolo personalizado

Entonces, ¿quieres crear tu propio protocolo de transporte, pero no sabes por dónde empezar? Tienes suerte, porque Glenn Fidler escribió dos artículos increíbles sobre esto. Encontrarás muchas ideas inteligentes en ellos.

El primer artículo, Networking for Game Programmers de 2008, es más sencillo que el segundo, Building A Game Network Protocol de 2016. Te recomiendo que comiences con el más antiguo.

Ten en cuenta que Glenn Fidler es un gran defensor del uso de un protocolo propio basado en UDP. Y después de leer sus artículos, seguramente adoptarás su opinión de que TCP tiene serias desventajas en los videojuegos y querrás implementar tu propio protocolo.

Pero si eres nuevo en el trabajo con redes, hazte un favor y utiliza TCP o una biblioteca. Para implementar con éxito tu propio protocolo de transporte, necesitas aprender muchas cosas primero.

Bibliotecas de red

Si necesitas algo más eficiente que TCP, pero no quieres complicarte con la implementación de tu propio protocolo y adentrarte en muchos detalles, puedes usar una biblioteca de red. Hay muchas disponibles:

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

Protocolo de transporte: conclusión

En resumen: existen dos protocolos de transporte principales: TCP y UDP. TCP tiene muchas características útiles: confiabilidad, preservación del orden de los paquetes y detección de errores. UDP no tiene nada de esto, pero TCP tiene naturalmente mayores latencias, inaceptables para algunos juegos. Para asegurar una baja latencia, se puede crear un protocolo propio basado en UDP o utilizar una biblioteca que implemente un protocolo de transporte sobre UDP adaptado para videojuegos multijugador.

La elección entre TCP, UDP y la biblioteca depende de varios factores. Primero, de las necesidades del juego: ¿requiere baja latencia? Segundo, de los requisitos del protocolo de la aplicación: ¿necesita un protocolo confiable? Como veremos en la siguiente parte, se puede crear un protocolo de aplicación para el cual un protocolo no confiable es perfectamente adecuado. Por último, también hay que tener en cuenta la experiencia del desarrollador del motor de red.

Tengo dos consejos:

  • Abstraiga al máximo el protocolo de transporte del resto de la aplicación, para que se pueda reemplazar fácilmente sin reescribir todo el código.
  • No se involucre en una optimización prematura. Si no es un especialista en redes y no está seguro de si necesita un protocolo de transporte propio basado en UDP, puede comenzar con TCP o una biblioteca que garantice la confiabilidad, y luego probar y medir el rendimiento. Si surgen problemas y está seguro de que la causa reside en el protocolo de transporte, entonces tal vez sea momento de crear su propio protocolo de transporte.

Para concluir esta parte, le recomiendo leer Introduction to Multiplayer Game Programming de Bryan Hook, donde se abordan muchos de los temas discutidos aquí.

Protocolo de aplicación

Ahora que podemos intercambiar datos entre clientes y el servidor, necesitamos decidir qué datos exactamente transmitir y en qué formato.

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

El servidor envía un estado filtrado y no completo con las entidades cercanas al jugador. Esto se hace por tres razones. Primero, el estado completo puede ser demasiado grande para transmitirse con alta frecuencia. En segundo lugar, a los clientes generalmente les interesa más la información visual y de audio, ya que la mayor parte de la lógica del juego se simula en el servidor de juego. En tercer lugar, en algunos juegos, el jugador no debe conocer ciertos datos, como la posición del oponente al otro lado del mapa, porque de lo contrario podría esnifar paquetes y saber exactamente hacia dónde moverse para matarlo.

Serialización

El primer paso será transformar los datos que queremos enviar (entrada o estado del juego) en un formato adecuado para la transmisión. Este proceso se llama serialización.

Inmediatamente podría parecer una buena idea usar un formato legible por humanos, como JSON o XML. Pero esto sería completamente ineficaz y ocuparía innecesariamente gran parte del canal.

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

Para la serialización de datos, se puede usar una biblioteca, por ejemplo:

Solo asegúrate de que la biblioteca genere archivos portátiles y se preocupe por el orden de bytes.

Una alternativa puede ser una implementación propia, que no es especialmente complicada, especialmente si utilizas un enfoque basado en datos en el código. Además, te permitirá realizar optimizaciones que no siempre son posibles al usar una biblioteca.

Glen Fidler escribió dos artículos sobre serialización: Lectura y escritura de paquetes y Estrategias de serialización.

Compresión

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

Empaquetado de bits

La primera técnica es el empaquetado de bits. Consiste en utilizar exactamente la cantidad de bits necesarios para describir la magnitud requerida. Por ejemplo, si tienes una enumeración que puede tener 16 valores diferentes, en lugar de un byte completo (8 bits), puedes usar solo 4 bits.

Glenn Fiddler explica cómo implementar esto en la segunda parte del artículo. Lectura y escritura de paquetes.

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

Discretización

Discretización es una técnica de compresión con pérdida que consiste en utilizar solo un subconjunto de los posibles valores para codificar la magnitud. La forma más sencilla de implementar la discretización es redondeando números de punto flotante.

Glenn Fiddler (¡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érdida.

Aquí, en mi opinión, hay tres algoritmos muy interesantes que debes conocer:

  • Codificación de Huffman de código precomputado, que es extremadamente rápido y puede ofrecer 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 el tamaño de los datos. Como se puede ver, aquí, se ha aplicado en una gran cantidad de áreas de aplicación. Para actualizaciones de estados puede resultar redundante. Pero puede ser útil si necesitas enviar activos, textos largos o relieve a los clientes desde el servidor.
  • Copia de series largas es probablemente el algoritmo de compresión más simple, pero es muy efectivo para ciertos tipos de datos, y puede usarse como una etapa de preprocesamiento antes de zlib. Es especialmente adecuado para comprimir relieve que consiste en mosaicos o vóxeles, donde muchos elementos adyacentes se repiten.

Compresión delta

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

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

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

Cifrado

Además, puede que necesites cifrar la transmisión de información entre clientes y servidores. Hay varias razones para ello:

  • privacidad/confidencialidad: los mensajes solo pueden ser leídos por el destinatario, y ninguna otra persona que esté realizando sniffing en la red podrá leerlos.
  • autenticación: la persona que desea actuar como jugador debe conocer su clave.
  • prevención del uso de trampas: a los jugadores malintencionados les será mucho más difícil crear sus propios paquetes para hacer trampas, tendrán que reproducir el esquema de cifrado y encontrar la clave (que cambia con cada conexión).

Recomiendo encarecidamente usar una biblioteca para esto. Sugeriría utilizar libsodium, porque es especialmente fácil de usar y tiene excelentes tutoriales. Especialmente interesante es el tutorial sobre intercambio de claves, que permite generar nuevas claves en cada nueva conexión.

Protocolo de la aplicación: conclusión

Con esto concluimos el protocolo de la aplicación. Creo que la compresión es completamente opcional y la decisión de utilizarla depende únicamente del juego y del ancho de banda requerido. El cifrado, en mi opinión, es obligatorio, pero se puede prescindir de él en el primer prototipo.

Lógica de la aplicación

Ahora somos capaces de actualizar el estado en el cliente, pero podemos encontrar problemas de latencia. El jugador, al realizar una entrada, necesita esperar la actualización del estado del juego desde el servidor para ver qué efecto tuvo en el mundo.

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

Existen varias técnicas que permiten mitigar el impacto de este problema, y en la siguiente sección hablaré sobre ellas.

Técnicas de suavizado de latencias

Todas las técnicas descritas en esta sección se analizan en detalle en la serie Fast-Paced Multiplayer de Gabriel Gambetta. Recomiendo encarecidamente leer esta magnífica serie de artículos. También incluye una demostración interactiva que permite ver cómo funcionan estas técnicas en la práctica.

La primera técnica consiste en aplicar el resultado de la entrada directamente, sin esperar la respuesta del servidor. Esto se llama pronosticación del lado del cliente.. Sin embargo, cuando el cliente recibe una actualización del servidor, debe asegurarse de que su predicción ha sido correcta. Si no lo es, solo necesita cambiar su estado de acuerdo con lo recibido del servidor, ya que el servidor es autoritario. Esta técnica fue utilizada por primera vez en Quake. Se puede leer más al respecto en el artículo Revisión del código del motor Quake de Fabien Sanglard [la traducción en Habr].

Un segundo grupo de técnicas se utiliza para suavizar el movimiento de otras entidades entre dos actualizaciones de estado. Hay dos formas de abordar esta tarea: interpolación y 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 causa una pequeña cantidad de retraso, ya que el cliente siempre ve lo que sucedió en el pasado. La extrapolación implica predecir dónde deberían estar ahora las entidades basándose en el último estado recibido por el cliente. Su desventaja es que, si una entidad cambia completamente de dirección, habrá un gran error entre la predicción y la posición real.

La última y más avanzada técnica, útil solo en FPS, es la compensación de retrasos. Al utilizar la compensación de retrasos, el servidor tiene en cuenta las latencias del cliente cuando dispara a un objetivo. Por ejemplo, si el jugador realizó un headshot en su pantalla, pero en realidad, debido al retraso, su objetivo estaba en otra posición, sería injusto negar al jugador el derecho a matar por causa del retraso. Por lo tanto, el servidor hace un retroceso en el tiempo hasta el momento en que el jugador disparó, para simular lo que el jugador vio en su pantalla y verificar la colisión entre su tiro y el objetivo.

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

Además, en la wiki de la compañía Valve hay dos artículos, Redes Multijugador de Source y Métodos de Compensación de Latencia en el Diseño y Optimización de Protocolos en el Juego Cliente/Servidor , que abordan la compensación de delays.

Prevención de trampas

Existen dos técnicas principales para prevenir trampas.

Primera: complicación en el envío de paquetes maliciosos por parte de los tramposos. Como se mencionó anteriormente, una buena manera de implementarlo es a través de la encriptación.

Segunda: el servidor autoritario solo debe recibir comandos/entradas/acciones. El cliente no debe tener la capacidad de cambiar el estado en el servidor, salvo envío de entradas. Entonces, cada vez que el servidor reciba una entrada, debe verificar su validez antes de aplicarla.

Lógica de la Aplicación: Conclusión

Te recomiendo implementar una forma de simular grandes retrasos y bajas frecuencias de actualización para poder probar el comportamiento de tu juego en malas condiciones, incluso cuando el cliente y el servidor se ejecutan en la misma computadora. Esto simplificará mucho la implementación de técnicas para suavizar retrasos.

Otros recursos útiles

Si deseas explorar otros recursos dedicados a modelos de red, puedes encontrarlos aquí:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster