Durante las últimas dos semanas he estado trabajando en un motor de red para mi juego. Antes de eso, no sabía nada sobre las tecnologías de red en los juegos, así que leí muchos artículos y realicé muchos experimentos para comprender todos los conceptos y poder escribir mi propio motor de red.
En esta guía, me gustaría compartir contigo varios conceptos que debes aprender antes de escribir tu propio motor de juego, así como los mejores recursos y artículos para aprenderlos.
En general, hay dos tipos principales de arquitecturas de red: punto a punto y cliente-servidor. En la arquitectura peer-to-peer (p2p), los datos se transfieren entre cualquier par de jugadores conectados, y 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 usa en algunos juegos, el estándar es cliente-servidor: es más fácil de implementar, requiere un ancho de canal más pequeño y facilita la protección contra las 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 un jugador piensa que está en coordenadas (10, 5), y el servidor le dice que está en (5, 3), entonces el cliente debe reemplazar su posición con la transmitida por el servidor, y no al revés. El uso de servidores autoritarios facilita el reconocimiento de los tramposos.
Hay tres componentes principales para 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 transfiere 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 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 y
UDP . Pero puede crear su propio protocolo de transporte basado en uno de ellos o usar la biblioteca en la que se usan.
Comparación de TCP y UDP
Tanto TCP como UDP están basados en
IP . IP le permite transferir un paquete desde la fuente al destinatario, pero no garantiza que el paquete enviado tarde o temprano llegue al destinatario, que lo alcanzará al menos una vez y que la secuencia de paquetes llegará en el orden correcto. Además, un paquete puede contener solo un tamaño de datos limitado especificado por el valor de
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 confiable y ordenada entre dos nodos con comprobación de errores. Por lo tanto, TCP es muy conveniente y se usa en muchos otros protocolos, por ejemplo, en
HTTP ,
FTP y
SMTP . Pero todas estas características tienen un precio:
retraso .
Para comprender por qué estas funciones pueden causar un retraso, debe comprender cómo funciona TCP. Cuando el nodo emisor reenvía el paquete al nodo receptor, espera recibir un acuse de recibo (ACK). Si después de cierto tiempo no lo recibe (porque se perdió el paquete o la confirmación, o por alguna otra razón), reenvía el paquete. Además, TCP garantiza que los paquetes se reciban en el orden correcto, por lo tanto, hasta que se reciba un paquete perdido, todos los demás paquetes no pueden procesarse, incluso si ya han sido recibidos por el nodo receptor.
Pero como probablemente entiendas, la demora en los juegos multijugador es muy importante, especialmente en géneros tan activos como 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 llegó al receptor. O puede procesar varias secuencias de datos para que un paquete perdido en una secuencia no ralentice las secuencias restantes. Por ejemplo, puede haber una transmisión para la entrada del jugador y otra transmisión para mensajes de chat. Si se pierde un mensaje de chat que no es información urgente, no ralentizará la entrada, lo cual es urgente. O bien, un protocolo propietario puede implementar la confiabilidad de manera diferente que en TCP para ser más eficiente en los videojuegos.
Entonces, si TCP es tan malo, ¿crearemos nuestro propio protocolo de transporte basado en UDP?
Todo es un poco más complicado. Aunque el TCP es casi subóptimo para los sistemas en red de juegos, puede funcionar bien en su juego y ahorrar su valioso tiempo. Por ejemplo, el retraso puede no ser un problema para un juego por turnos o un juego que solo se puede jugar en LAN, donde hay mucho menos retrasos y pérdida de paquetes que en Internet.
Muchos juegos exitosos, incluidos World of Warcraft, Minecraft y Terraria, usan TCP. Sin embargo, la mayoría de los FPS utilizan protocolos patentados basados en UDP, por lo que hablaremos más sobre ellos a continuación.
Si decide usar TCP, asegúrese de que
el algoritmo de Nagle esté desactivado, ya que almacena 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, puede leer el artículo de Glenn Fiedler
UDP vs. TCPProtocolo propio
Entonces, ¿desea crear su propio protocolo de transporte, pero no sabe por dónde empezar? Tienes suerte, porque Glenn Fiedler escribió dos artículos increíbles sobre esto. Encontrarás muchos pensamientos inteligentes en ellos.
El primer artículo,
Networking for Game Programmers 2008, es más simple que el segundo,
Building A Game Network Protocol 2016. Te recomiendo que comiences con una más antigua.
Tenga en cuenta que Glenn Fiedler es un gran defensor del uso de su propio protocolo UDP. Y después de leer sus artículos, seguramente superará su opinión de que TCP tiene serios inconvenientes en los videojuegos, y desea implementar su propio protocolo.
Pero si eres nuevo en redes, hazte un favor y usa TCP o una biblioteca. Para implementar con éxito su propio protocolo de transporte, primero debe aprender mucho.
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, ella tiene documentación clara y un tutorial para principiantes.
Protocolo de transporte: conclusión
Para resumir: hay dos protocolos principales de transporte: TCP y UDP. TCP tiene muchas características útiles: confiabilidad, orden de paquetes, detección de errores. UDP no tiene todo esto, pero TCP, por su naturaleza, ha aumentado los retrasos que son inaceptables para algunos juegos. Es decir, para garantizar bajas latencias, puede crear su propio protocolo basado en UDP o utilizar una biblioteca que implemente el protocolo de transporte UDP y que esté adaptada para videojuegos para múltiples jugadores.
La elección entre TCP, UDP y la biblioteca depende de varios factores. En primer lugar, según las necesidades del juego: ¿necesita bajas latencias? En segundo lugar, a partir de los requisitos del protocolo de aplicación: ¿necesita un protocolo confiable? Como veremos en la siguiente parte, puede crear un protocolo de aplicación para el cual un protocolo poco confiable es bastante adecuado. Finalmente, también debe tener en cuenta la experiencia del desarrollador del motor de red.
Tengo dos consejos:
- Maximice el protocolo de transporte del resto de la aplicación para que pueda reemplazarse fácilmente sin tener que volver a escribir todo el código.
- No hagas una optimización prematura. Si no es un especialista 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 brinde confiabilidad, y luego probar y medir el rendimiento. Si tiene problemas y está seguro de que la razón radica en el protocolo de transporte, quizás haya llegado el momento de crear su propio protocolo de transporte.
Al final de esta parte, le recomiendo que lea la
Introducción a la programación de juegos multijugador de Brian Hook, que cubre muchos de los temas discutidos aquí.
Protocolo de aplicación
Ahora que podemos intercambiar datos entre 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 un estado completo, pero filtrado, con entidades que están al lado del jugador. Lo hace por tres razones. Primero, el estado general puede ser demasiado grande para la transmisión de 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 conocer ciertos datos, por ejemplo, la posición del oponente en el otro extremo del mapa, porque de lo contrario puede oler paquetes y saber exactamente dónde moverse para matarlo.
Serialización
El primer paso es convertir los datos que queremos enviar (entrada o estado del juego) a un formato adecuado para la transmisión. Este proceso se llama
serialización .
La idea viene inmediatamente a la mente para usar un formato legible por humanos, como JSON o XML. Pero será completamente ineficaz y en vano ocupará la mayor parte del canal.
En cambio, se recomienda que utilice un formato binario que sea mucho más compacto. Es decir, los paquetes contendrán solo unos pocos bytes. Aquí debe considerar el problema
del orden de bytes , que puede diferir en diferentes computadoras.
Puede usar una biblioteca para serializar datos, por ejemplo:
Solo asegúrese de que la biblioteca cree archivos portátiles y se encargue del orden de los bytes.
Una solución independiente puede ser una implementación independiente, no es particularmente complicada, especialmente si utiliza un enfoque orientado a datos en el código. Además, le permitirá realizar optimizaciones que no siempre son posibles al usar la biblioteca.
Glenn Fiedler ha escrito dos artículos sobre serialización:
paquetes de lectura y escritura y
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 permite transferir más datos en cada instantánea, aumentar la frecuencia de actualización o simplemente reducir los requisitos del canal.
Poco de embalaje
La primera técnica es un poco de embalaje. Consiste en usar exactamente el número de bits que es necesario 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 empaque de bits funciona especialmente bien con el muestreo, que será el tema de la próxima sección.
Discreción
La discretización es una técnica de compresión con pérdida que usa solo un subconjunto de los valores posibles para codificar un valor. La forma más fácil de implementar la discretización es redondear los números de coma flotante.
Glenn Fiedler (¡otra vez!) Muestra cómo aplicar el muestreo en la práctica en su artículo de
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, los tres algoritmos más interesantes que necesita saber:
- Codificación de Huffman con código precalculado que es extremadamente rápido y puede dar buenos resultados. Se usó 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 se puede ver aquí , se ha utilizado en muchas aplicaciones. Puede ser redundante actualizar estados. Pero puede ser útil si necesita enviar activos, textos largos o ayuda a los clientes desde el servidor.
- Copiar longitudes de series es probablemente el algoritmo de compresión más simple, pero es muy efectivo para ciertos tipos de datos y puede usarse como un paso de preprocesamiento antes de zlib. Es especialmente adecuado para comprimir terrenos que consisten en azulejos 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 basa 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 usó en la segunda parte de su artículo de
Compresión de instantáneas .
Cifrado
Además, es posible que deba cifrar la transferencia 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 ninguna otra persona que detecte la red puede leerlos.
- autenticación: una persona que quiere desempeñar 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 jugar el esquema de cifrado y encontrar la clave (que cambia con cada conexión).
Recomiendo usar la biblioteca para esto. Sugiero usar
libsodium porque es especialmente simple y tiene excelentes tutoriales. De particular interés es el tutorial de
intercambio de claves , que le permite generar nuevas claves con cada nueva conexión.
Protocolo de aplicación: conclusión
Terminaremos con 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 hacerlo sin él.
Lógica de aplicación
Ahora podemos actualizar el estado en el cliente, pero podemos encontrar problemas con demoras. Una vez ingresado, el jugador debe esperar la actualización del estado del juego desde el servidor para ver qué impacto tuvo en el mundo.
Además, entre dos actualizaciones de estado, el mundo es completamente estático. Si la frecuencia de actualización de los estados es baja, entonces los movimientos serán muy nerviosos.
Existen varias técnicas para reducir el impacto de este problema, y en la siguiente sección hablaré sobre ellas.
Técnicas de suavizado diferido
Todas las técnicas descritas en esta sección se analizan en detalle en la serie
Multijugador de ritmo rápido de Gabriel Gambetta. Recomiendo leer esta gran serie de artículos. También tiene una demostración interactiva que le permite ver cómo funcionan estas técnicas en la práctica.
La primera técnica es aplicar la entrada directamente, sin esperar una respuesta del servidor. Esto se llama
predicción del lado del cliente . Sin embargo, cuando el cliente recibe la actualización del servidor, debe asegurarse de que su pronóstico sea correcto. Si esto no es así, entonces solo necesita cambiar su estado de acuerdo con el recibido del servidor, porque el servidor es autoritario. Esta técnica se usó por primera vez en Quake. Puede leer más al respecto en el artículo
Revisión del código de Quake Engine por Fabien Sanglar [
traducción al 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: 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 fracción de la demora, porque el cliente siempre ve lo que sucedió en el pasado. La extrapolación predice dónde las entidades ahora deberían basarse en el último estado recibido por el cliente. Su desventaja es que si la entidad cambia completamente la dirección del movimiento, entonces 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 la
compensación de retraso . , . , , - , - . , , , , , .
( !) 2004
Network Physics (2004) , . 2014
Networking Physics , .
wiki Valve ,
Source Multiplayer Networking Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization .
.
: . , .
: //. , . .
:
, , . .
, , :