Sobre o modelo de rede en xogos para principiantes

Sobre o modelo de rede en xogos para principiantes
Durante as últimas dúas semanas estiven traballando no motor de rede para o meu xogo. Antes diso, non sabía nada de redes nos xogos, polo que lin moitos artigos e fixen moitos experimentos para comprender todos os conceptos e poder escribir o meu propio motor de redes.

Nesta guía, gustaríame compartir contigo os distintos conceptos que debes aprender antes de escribir o teu propio motor de xogos, así como os mellores recursos e artigos para aprendelos.

En xeral, hai dous tipos principais de arquitecturas de rede: peer-to-peer e cliente-servidor. Nunha arquitectura peer-to-peer (p2p), os datos transfírense entre calquera par de reprodutores conectados, mentres que nunha arquitectura cliente-servidor, os datos transfírense só entre os xogadores e o servidor.

Aínda que a arquitectura peer-to-peer aínda se usa nalgúns xogos, o cliente-servidor é o estándar: é máis fácil de implementar, require un ancho de canle menor e facilita a protección contra as trampas. Polo tanto, nesta guía centrarémonos na arquitectura cliente-servidor.

En particular, nos interesan máis os servidores autoritarios: nestes sistemas, o servidor sempre ten razón. Por exemplo, se o xogador cre que está en (10, 5) e o servidor dille que está en (5, 3), entón o cliente debería substituír a súa posición pola que o servidor está informando, non ao revés. O uso de servidores autorizados facilita o recoñecemento de tramposos.

Hai tres compoñentes principais nos sistemas de rede de xogos:

  • Protocolo de transporte: como se transfiren os datos entre os clientes e o servidor.
  • Protocolo de aplicación: que se transmite dos clientes ao servidor e do servidor aos clientes, e en que formato.
  • Lóxica de aplicación: como se usan os datos transmitidos para actualizar o estado dos clientes e do servidor.

É moi importante comprender o papel de cada parte e as dificultades asociadas a elas.

Protocolo de transporte

O primeiro paso é escoller un protocolo para o transporte de datos entre o servidor e os clientes. Existen dous protocolos de Internet para iso: TCP и UDP. Pero podes crear o teu propio protocolo de transporte baseado nun deles ou usar unha biblioteca que os utilice.

Comparación de TCP e UDP

Tanto TCP como UDP están baseados IP. IP permite transmitir un paquete desde unha fonte a un receptor, pero non garante que o paquete enviado chegue tarde ou cedo ao receptor, que chegue a el polo menos unha vez e que a secuencia de paquetes chegue na orde correcta. Ademais, un paquete só pode conter un tamaño de datos limitado, dado polo valor MTU.

UDP é só unha capa delgada sobre a IP. Polo tanto, ten as mesmas limitacións. Pola contra, TCP ten moitas características. Ofrece unha conexión ordenada fiable entre dous nodos con comprobación de erros. Polo tanto, TCP é moi cómodo e úsase en moitos outros protocolos, por exemplo, en HTTP, FTP и SMTP. Pero todas estas características teñen un prezo: atraso.

Para entender por que estas funcións poden causar latencia, necesitamos entender como funciona TCP. Cando o host emisor transmite un paquete ao host receptor, espera recibir un acuse de recibo (ACK). Se despois dun certo tempo non o recibe (porque se perdeu o paquete ou a confirmación, ou por algún outro motivo), entón envía o paquete de novo. Ademais, TCP garante que os paquetes se reciben na orde correcta, polo que ata que non se reciba un paquete perdido, todos os demais paquetes non poden ser procesados, aínda que xa fosen recibidos polo nodo receptor.

Pero como probablemente entenderás, a latencia nos xogos multixogador é moi importante, especialmente en xéneros tan activos como o FPS. É por iso que moitos xogos usan UDP co seu propio protocolo.

Un protocolo nativo baseado en UDP pode ser máis eficiente que TCP por varios motivos. Por exemplo, pode marcar algúns paquetes como de confianza e outros como non fiables. Polo tanto, non lle importa se o paquete non fiable chegou ao destinatario. Ou pode procesar varios fluxos de datos para que un paquete perdido nun fluxo non ralentice outros fluxos. Por exemplo, pode haber un fío para a entrada do xogador e outro para as mensaxes de chat. Se se perde unha mensaxe de chat que non son datos urxentes, non ralentizará a entrada que é urxente. Ou un protocolo propietario pode implementar a fiabilidade de forma diferente que TCP para ser máis eficiente nun ambiente de videoxogos.

Entón, se TCP é unha merda, entón imos construír o noso propio protocolo de transporte baseado en UDP?

Todo é un pouco máis complicado. Aínda que o TCP é case subóptimo para os sistemas de rede de xogos, pode funcionar bastante ben para o teu xogo específico e aforrarche un tempo valioso. Por exemplo, a latencia pode non ser un problema para un xogo por quendas ou un xogo que só se pode xogar en redes LAN, onde a latencia e a perda de paquetes son moito menores que en Internet.

Moitos xogos exitosos, incluídos World of Warcraft, Minecraft e Terraria, usan TCP. Non obstante, a maioría dos FPS usan os seus propios protocolos baseados en UDP, polo que falaremos máis sobre eles a continuación.

Se decides usar TCP, asegúrate de que estea desactivado Algoritmo de Nagle, porque almacena os paquetes antes de enviar, o que significa que aumenta o atraso.

Para obter máis información sobre as diferenzas entre UDP e TCP no contexto dos xogos multixogador, consulte o artigo de Glenn Fiedler UDP vs. TCP.

Protocolo propietario

Entón, queres crear o teu propio protocolo de transporte pero non sabes por onde comezar? Estás de sorte, porque Glenn Fiedler escribiu dous artigos incribles sobre iso. Neles atoparás moitas ideas intelixentes.

Primeiro artigo Redes para programadores de xogos 2008, máis doado que o segundo Construír un protocolo de rede de xogos 2016. Recoméndovos que comecedes polo máis vello.

Teña en conta que Glenn Fiedler é un gran defensor do uso do seu propio protocolo baseado en UDP. E despois de ler os seus artigos, probablemente adoptará a súa opinión de que TCP ten serios inconvenientes nos videoxogos e quererá implementar o seu propio protocolo.

Pero se es novo na rede, fai un favor e usa TCP ou unha biblioteca. Para implementar con éxito o teu propio protocolo de transporte, necesitas aprender moito de antemán.

Bibliotecas en Rede

Se necesitas algo máis eficiente que TCP, pero non queres molestar en implementar o teu propio protocolo e entrar en moitos detalles, podes usar a biblioteca de rede. Hai moitos deles:

Non os probei todos, pero prefiro ENet porque é fácil de usar e fiable. Ademais, conta cunha documentación clara e un titorial para principiantes.

Conclusión do protocolo de transporte

En resumo, hai dous protocolos de transporte principais: TCP e UDP. TCP ten moitas características útiles: fiabilidade, preservación da orde de paquetes, detección de erros. UDP non ten todo iso, pero TCP, pola súa propia natureza, ten unha alta latencia que é inaceptable para algúns xogos. É dicir, para garantir unha baixa latencia, podes crear o teu propio protocolo baseado en UDP ou utilizar unha biblioteca que implemente o protocolo de transporte en UDP e que estea adaptada para videoxogos multixogador.

A elección entre TCP, UDP e a biblioteca depende de varios factores. En primeiro lugar, a partir das necesidades do xogo: precisa de baixa latencia? En segundo lugar, a partir dos requisitos do protocolo de aplicación: necesita un protocolo fiable? Como veremos na seguinte parte, é posible crear un protocolo de aplicación para o que un protocolo pouco fiable é bastante adecuado. Finalmente, tamén debes ter en conta a experiencia do desenvolvedor do motor de rede.

Teño dous consellos:

  • Abstraer o protocolo de transporte o máximo posible do resto da aplicación para que se poida substituír facilmente sen reescribir todo o código.
  • Non optimices demasiado. Se non es un experto en redes e non está seguro de se necesita o seu propio protocolo de transporte baseado en UDP, pode comezar con TCP ou unha biblioteca que ofreza fiabilidade e despois probar e medir o rendemento. Se tes problemas e estás seguro de que se trata dun protocolo de transporte, quizais sexa o momento de crear o teu propio protocolo de transporte.

Ao final desta parte, recoméndoche que leas Introdución á programación de xogos multixogador Brian Hook, que abarca moitos dos temas tratados aquí.

Protocolo de aplicación

Agora que podemos intercambiar datos entre os clientes e o servidor, temos que decidir que datos transferir e en que formato.

O esquema clásico é que os clientes envían entradas ou accións ao servidor, e o servidor envía o estado actual do xogo aos clientes.

O servidor non envía o estado completo, senón o filtrado con entidades que están preto do reprodutor. Faino por tres motivos. En primeiro lugar, o estado total pode ser demasiado grande para transmitir a alta frecuencia. En segundo lugar, os clientes están interesados ​​principalmente en datos visuais e de audio, porque a maior parte da lóxica do xogo está simulada no servidor do xogo. En terceiro lugar, nalgúns xogos o xogador non precisa coñecer certos datos, como a posición do inimigo no outro lado do mapa, porque, se non, pode cheirar paquetes e saber exactamente onde moverse para matalo.

Serialización

O primeiro paso é converter os datos que queremos enviar (entrada ou estado do xogo) nun formato adecuado para a súa transmisión. Este proceso chámase serialización.

Inmediatamente vén á mente a idea de usar un formato lexible por humanos, como JSON ou XML. Pero isto será completamente ineficiente e ocupará a maior parte da canle por nada.

En cambio, recoméndase utilizar o formato binario, que é moito máis compacto. É dicir, os paquetes só conterán uns poucos bytes. Aquí hai que ter en conta o problema orde de bytes, que poden diferir en diferentes ordenadores.

Para serializar datos, pode usar unha biblioteca, por exemplo:

Só asegúrate de que a biblioteca crea arquivos portátiles e coida a endianidade.

Unha solución alternativa sería implementala vostede mesmo, non é tan difícil, especialmente se está a usar un enfoque centrado nos datos no seu código. Ademais, permitirache realizar optimizacións que non sempre son posibles ao utilizar a biblioteca.

Glenn Fiedler escribiu dous artigos sobre a serialización: Paquetes de lectura e escritura и Estratexias de serialización.

Compresión

A cantidade de datos transferidos entre os clientes e o servidor está limitada polo ancho de banda da canle. A compresión de datos permitirá transferir máis datos en cada instantánea, aumentar a frecuencia de actualización ou simplemente reducir os requisitos de ancho de banda.

Embalaxe de bits

A primeira técnica é o empaquetado de bits. Consiste en utilizar exactamente o número de bits necesario para describir o valor desexado. Por exemplo, se tes unha enumeración que pode ter 16 valores diferentes, en lugar dun byte enteiro (8 bits), podes usar só 4 bits.

Glenn Fiedler explica como implementar isto na segunda parte do artigo. Paquetes de lectura e escritura.

O empaquetado de bits funciona especialmente ben coa discretización, que será o tema da seguinte sección.

Mostraxe

Mostraxe é unha técnica de compresión con perdas que usa só un subconxunto de valores posibles para codificar un valor. A forma máis sinxela de implementar a discretización é redondeando números de coma flotante.

Glenn Fiedler (de novo!) mostra como aplicar a discretización na práctica no seu artigo Compresión de instantáneas.

Algoritmos de compresión

A seguinte técnica serán os algoritmos de compresión sen perdas.

Aquí, na miña opinión, están os tres algoritmos máis interesantes que debes coñecer:

  • Codificación de Huffman con código precalculado, que é extremadamente rápido e pode producir bos resultados. Utilizouse para comprimir paquetes no motor de rede Quake3.
  • zlib é un algoritmo de compresión de propósito xeral que nunca aumenta a cantidade de datos. Como podes ver aquí, utilizouse nunha variedade de aplicacións. Para actualizar estados, pode ser redundante. Pero pode ser útil se precisa enviar activos, textos longos ou terreos aos clientes desde o servidor.
  • Copiando lonxitudes de tiradas Probablemente é o algoritmo de compresión máis sinxelo, pero é moi eficiente para certos tipos de datos e pódese usar como un paso de pre-procesamento antes de zlib. É especialmente indicado para comprimir terreos formados por tellas ou voxels nos que se repiten moitos elementos veciños.

compresión delta

A técnica de compresión final é a compresión delta. Está no feito de que só se transmiten as diferenzas entre o estado actual do xogo e o último estado recibido polo cliente.

Utilizouse por primeira vez no motor de rede Quake3. Aquí tes dous artigos que explican como usalo:

Glenn Fiedler tamén o usou na segunda parte do seu artigo. Compresión de instantáneas.

Cifrado

Ademais, é posible que necesites cifrar a transmisión de información entre os clientes e o servidor. Hai varias razóns para iso:

  • Privacidade/Confidencialidade: as mensaxes só poden ler o destinatario e ningún outro detector de rede poderá lelas.
  • autenticación: unha persoa que quere desempeñar o papel de xogador debe coñecer a súa clave.
  • prevención de trampas: será moito máis difícil para os xogadores maliciosos crear os seus propios paquetes de trampas, terán que replicar o esquema de cifrado e atopar a clave (que cambia en cada conexión).

Recomendo encarecidamente usar unha biblioteca para iso. Suxiro usar libsodio, porque é especialmente sinxelo e ten excelentes titoriais. Particularmente interesante é o tutorial sobre intercambio de claves, que lle permite xerar novas claves en cada nova conexión.

Protocolo de aplicación: conclusión

Conclúe así o protocolo de aplicación. Creo que a compresión é completamente opcional e que a decisión de usala depende só do xogo e do ancho de banda necesario. O cifrado, na miña opinión, é obrigatorio, pero no primeiro prototipo pódese prescindir del.

Lóxica de aplicación

Agora podemos actualizar o estado do cliente, pero é posible que teñamos problemas de latencia. O xogador, despois de facer unha entrada, ten que esperar a unha actualización do estado do xogo do servidor para ver o efecto que tivo no mundo.

Ademais, entre dúas actualizacións de estado, o mundo está completamente estático. Se a taxa de actualización do estado é baixa, os movementos serán moi bruscos.

Existen varias técnicas para mitigar o impacto deste problema, e cubrireias na seguinte sección.

Técnicas de alisado retardado

Todas as técnicas descritas nesta sección son discutidas en detalle na serie. Multixogador de ritmo rápido Gabriel Gambetta. Recomendo encarecidamente a lectura desta excelente serie de artigos. Tamén inclúe unha demostración interactiva para ver como funcionan estas técnicas na práctica.

A primeira técnica é aplicar directamente o resultado da entrada sen esperar a resposta do servidor. Chámase predición do lado do cliente. Non obstante, cando o cliente recibe unha actualización do servidor, debe verificar que a súa predición foi correcta. Se este non é o caso, entón só necesita cambiar o seu estado segundo o que recibiu do servidor, porque o servidor é autoritario. Esta técnica utilizouse por primeira vez en Quake. Podes ler máis sobre iso no artigo. Revisión do código de Quake Engine Fabien Sanglars [tradución sobre Habré].

O segundo conxunto de técnicas úsase para suavizar o movemento doutras entidades entre dúas actualizacións de estado. Hai dúas formas de resolver este problema: interpolación e extrapolación. No caso da interpolación, tómanse os dous últimos estados e móstrase a transición dun a outro. A súa desvantaxe é que provoca unha pequena fracción do atraso, porque o cliente sempre ve o que pasou no pasado. A extrapolación consiste en predecir onde deberían estar agora as entidades en función do último estado recibido polo cliente. A súa desvantaxe é que se a entidade cambia completamente a dirección do movemento, entón haberá un gran erro entre a previsión e a posición real.

A última técnica máis avanzada, útil só en FPS, é compensación de atraso. Ao usar a compensación de atraso, o servidor ten en conta os atrasos do cliente cando dispara contra o obxectivo. Por exemplo, se un xogador realizaba un tiro á cabeza na súa pantalla, pero en realidade o seu obxectivo estaba nun lugar diferente debido ao atraso, sería inxusto negarlle o dereito a matar debido ao atraso. Así, o servidor rebobina o tempo ata cando o xogador disparou para simular o que o xogador viu na súa pantalla e comprobar se hai colisión entre o seu disparo e o obxectivo.

Glenn Fiedler (como sempre!) escribiu un artigo en 2004 Física de redes (2004), no que sentou as bases para a sincronización de simulacións físicas entre o servidor e o cliente. En 2014 escribiu unha nova serie de artigos física de redes, no que describiu outras técnicas para sincronizar simulacións físicas.

Tamén hai dous artigos na wiki de Valve, Rede multixogador de orixe и Métodos de compensación da latencia no deseño e optimización do protocolo no xogo cliente/servidor tratar a compensación por atraso.

Prevención de trampas

Hai dúas técnicas principais de prevención de trampas.

En primeiro lugar, dificultando que os tramposos envíen paquetes maliciosos. Como se mencionou anteriormente, unha boa forma de implementalo é o cifrado.

En segundo lugar, o servidor autorizado só debería recibir comandos/entradas/accións. O cliente non debería poder cambiar o estado no servidor senón enviando entrada. Entón, o servidor, cada vez que recibe entrada, debe comprobar a súa validez antes de aplicala.

Lóxica de aplicación: conclusión

Recomendo que implementes un xeito de simular altas latencias e baixas taxas de actualización para que poidas probar o comportamento do teu xogo en malas condicións, mesmo cando o cliente e o servidor estean executando na mesma máquina. Isto simplifica moito a implementación de técnicas de suavización do retardo.

Outros recursos útiles

Se queres explorar outros recursos de modelos de rede, podes atopalos aquí:

Fonte: www.habr.com

Engadir un comentario