Sobre o modelo de rede em jogos para iniciantes

Sobre o modelo de rede em jogos para iniciantes
Nas últimas duas semanas tenho trabalhado no motor online do meu jogo. Antes disso, eu não sabia nada sobre redes em jogos, então li muitos artigos e fiz muitos experimentos para entender todos os conceitos e poder escrever meu próprio mecanismo de rede.

Neste guia, gostaria de compartilhar com você os vários conceitos que você precisa aprender antes de escrever seu próprio mecanismo de jogo, bem como os melhores recursos e artigos para aprendê-los.

Em geral, existem dois tipos principais de arquiteturas de rede: ponto a ponto e cliente-servidor. Em uma arquitetura peer-to-peer (p2p), os dados são transferidos entre quaisquer pares de jogadores conectados, enquanto em uma arquitetura cliente-servidor, os dados são transferidos apenas entre os jogadores e o servidor.

Embora a arquitetura peer-to-peer ainda seja usada em alguns jogos, o cliente-servidor é o padrão: é mais fácil de implementar, requer uma largura de canal menor e facilita a proteção contra trapaças. Portanto, neste tutorial focaremos na arquitetura cliente-servidor.

Em particular, estamos mais interessados ​​em servidores autoritários: em tais sistemas, o servidor tem sempre razão. Por exemplo, se um jogador pensa que está nas coordenadas (10, 5), e o servidor lhe diz que ele está nas coordenadas (5, 3), então o cliente deve substituir sua posição pela reportada pelo servidor, e não vice-versa. vice-versa. O uso de servidores autorizados facilita a identificação de trapaceiros.

Os sistemas de jogos em rede têm três componentes principais:

  • Protocolo de transporte: como os dados são transferidos entre clientes e servidor.
  • Protocolo de aplicação: o que é transmitido dos clientes para o servidor e do servidor para os clientes e em que formato.
  • Lógica da aplicação: como os dados transferidos são usados ​​para atualizar o estado dos clientes e do servidor.

É muito importante compreender o papel de cada parte e os desafios a ela associados.

Protocolo de transporte

A primeira etapa é selecionar um protocolo para transporte de dados entre o servidor e os clientes. Existem dois protocolos de Internet para isso: TCP и UDP. Mas você pode criar seu próprio protocolo de transporte baseado em um deles ou usar uma biblioteca que os utilize.

Comparação de TCP e UDP

Tanto o TCP quanto o UDP são baseados em IP. O IP permite que um pacote seja transmitido de uma origem para um destinatário, mas não garante que o pacote enviado chegará mais cedo ou mais tarde ao destinatário, que o alcançará pelo menos uma vez e que a sequência de pacotes chegará no correto ordem. Além disso, um pacote só pode conter uma quantidade limitada de dados, dada por MTU.

O UDP é apenas uma camada fina sobre o IP. Portanto, tem as mesmas limitações. Em contraste, o TCP possui muitos recursos. Ele fornece uma conexão confiável e ordenada entre dois nós com verificação de erros. Conseqüentemente, o TCP é muito conveniente e é usado em muitos outros protocolos, por ex. HTTP, FTP и SMTP. Mas todos esses recursos têm um preço: demora.

Para entender por que essas funções podem causar latência, precisamos entender como funciona o TCP. Quando um nó emissor transmite um pacote para um nó receptor, ele espera receber uma confirmação (ACK). Se depois de um certo tempo ele não o receber (porque o pacote ou a confirmação foi perdido, ou por algum outro motivo), então ele reenvia o pacote. Além disso, o TCP garante que os pacotes sejam recebidos na ordem correta, de modo que, até que o pacote perdido seja recebido, todos os outros pacotes não poderão ser processados, mesmo que já tenham sido recebidos pelo host receptor.

Mas como você provavelmente pode imaginar, a latência em jogos multijogador é muito importante, especialmente em gêneros cheios de ação como FPS. É por isso que muitos jogos usam UDP com seu próprio protocolo.

Um protocolo nativo baseado em UDP pode ser mais eficiente que o TCP por vários motivos. Por exemplo, pode marcar alguns pacotes como confiáveis ​​e outros como não confiáveis. Portanto, não importa se o pacote não confiável chega ao destinatário. Ou pode processar vários fluxos de dados para que um pacote perdido em um fluxo não diminua a velocidade dos fluxos restantes. Por exemplo, pode haver um tópico para entrada do jogador e outro tópico para mensagens de bate-papo. Se uma mensagem de bate-papo que não é urgente for perdida, isso não retardará a entrada urgente. Ou um protocolo proprietário pode implementar confiabilidade de forma diferente do TCP para ser mais eficiente em um ambiente de videogame.

Então, se o TCP é tão ruim, criaremos nosso próprio protocolo de transporte baseado em UDP?

É um pouco mais complicado. Embora o TCP seja quase abaixo do ideal para sistemas de rede de jogos, ele pode funcionar muito bem para o seu jogo específico e economizar um tempo valioso. Por exemplo, a latência pode não ser um problema para um jogo baseado em turnos ou para um jogo que só pode ser jogado em redes LAN, onde a latência e a perda de pacotes são muito mais baixas do que na Internet.

Muitos jogos de sucesso, incluindo World of Warcraft, Minecraft e Terraria, usam TCP. No entanto, a maioria dos FPSs usa seus próprios protocolos baseados em UDP, por isso falaremos mais sobre eles a seguir.

Se você decidir usar TCP, certifique-se de que esteja desabilitado Algoritmo de Nagle, porque armazena os pacotes em buffer antes de enviá-los, o que significa que aumenta a latência.

Para saber mais sobre as diferenças entre UDP e TCP no contexto de jogos multijogador, você pode ler o artigo de Glenn Fiedler UDP versus TCP.

Protocolo próprio

Quer criar seu próprio protocolo de transporte, mas não sabe por onde começar? Você está com sorte porque Glenn Fiedler escreveu dois artigos incríveis sobre isso. Você encontrará muitos pensamentos inteligentes neles.

O primeiro artigo Networking para programadores de jogos 2008, mais fácil que o segundo, Construindo um protocolo de rede de jogos 2016. Eu recomendo que você comece com o mais antigo.

Observe que Glenn Fiedler é um grande defensor do uso de um protocolo personalizado baseado em UDP. E depois de ler seus artigos, você provavelmente adotará a opinião dele de que o TCP tem sérias deficiências em videogames e desejará implementar seu próprio protocolo.

Mas se você é novo em redes, faça um favor e use o TCP ou uma biblioteca. Para implementar com sucesso seu próprio protocolo de transporte, você precisa aprender muito de antemão.

Bibliotecas de rede

Se você precisa de algo mais eficiente que o TCP, mas não quer se preocupar em implementar seu próprio protocolo e entrar em muitos detalhes, você pode usar uma biblioteca de rede. Existem muitos deles:

Ainda não experimentei todos, mas prefiro o ENet porque é fácil de usar e confiável. Além disso, possui documentação clara e um tutorial para iniciantes.

Protocolo de Transporte: Conclusão

Resumindo: existem dois protocolos de transporte principais: TCP e UDP. O TCP possui muitos recursos úteis: confiabilidade, preservação da ordem dos pacotes, detecção de erros. O UDP não tem tudo isso, mas o TCP, por sua natureza, aumentou a latência, o que é inaceitável para alguns jogos. Ou seja, para garantir baixa latência, você pode criar seu próprio protocolo baseado em UDP ou utilizar uma biblioteca que implemente um protocolo de transporte em UDP e esteja adaptada para videogames multiplayer.

A escolha entre TCP, UDP e biblioteca depende de vários fatores. Primeiro, pelas necessidades do jogo: ele precisa de baixa latência? Em segundo lugar, a partir dos requisitos do protocolo de aplicação: é necessário um protocolo confiável? Como veremos na próxima parte, é possível criar um protocolo de aplicação para o qual um protocolo não confiável seja bastante adequado. Finalmente, você também precisa levar em consideração a experiência do desenvolvedor do mecanismo de rede.

Tenho dois conselhos:

  • Abstraia o máximo possível o protocolo de transporte do resto da aplicação para que ele possa ser facilmente substituído sem reescrever todo o código.
  • Não otimize demais. Se você não for um especialista em redes e não tiver certeza se precisa de um protocolo de transporte personalizado baseado em UDP, poderá começar com TCP ou uma biblioteca que forneça confiabilidade e, em seguida, testar e medir o desempenho. Se surgirem problemas e você tiver certeza de que a causa é o protocolo de transporte, talvez seja hora de criar seu próprio protocolo de transporte.

No final desta parte, recomendo que você leia Introdução à programação de jogos multijogador por Brian Hook, que cobre muitos dos tópicos discutidos aqui.

Protocolo de aplicação

Agora que podemos trocar dados entre clientes e servidores, precisamos decidir quais dados transferir e em que formato.

O esquema clássico é que os clientes enviam entradas ou ações ao servidor, e o servidor envia o estado atual do jogo aos clientes.

O servidor não envia o estado completo, mas um estado filtrado com entidades localizadas próximas ao player. Ele faz isso por três razões. Primeiro, o estado completo pode ser muito grande para ser transmitido em alta frequência. Em segundo lugar, os clientes estão interessados ​​principalmente em dados visuais e de áudio, porque a maior parte da lógica do jogo é simulada no servidor do jogo. Em terceiro lugar, em alguns jogos o jogador não precisa saber certos dados, por exemplo, a posição do inimigo do outro lado do mapa, caso contrário ele pode farejar pacotes e saber exatamente para onde se mover para matá-lo.

Serialização

O primeiro passo é converter os dados que queremos enviar (entrada ou estado do jogo) em um formato adequado para transmissão. Este processo é chamado serialização.

A ideia que vem imediatamente à mente é usar um formato legível por humanos, como JSON ou XML. Mas isso será completamente ineficaz e desperdiçará a maior parte do canal.

Recomenda-se usar o formato binário, que é muito mais compacto. Ou seja, os pacotes conterão apenas alguns bytes. Há um problema a considerar aqui ordem de bytes, que pode diferir em computadores diferentes.

Para serializar dados, você pode usar uma biblioteca, por exemplo:

Apenas certifique-se de que a biblioteca crie arquivos portáteis e se preocupe com o endianismo.

Uma solução alternativa é implementá-lo você mesmo; não é particularmente difícil, especialmente se você usar uma abordagem centrada em dados para seu código. Além disso, permitirá realizar otimizações que nem sempre são possíveis ao utilizar a biblioteca.

Glenn Fiedler escreveu dois artigos sobre serialização: Lendo e escrevendo pacotes и Estratégias de serialização.

compressão

A quantidade de dados transferidos entre clientes e servidor é limitada pela largura de banda do canal. A compactação de dados permitirá transferir mais dados em cada instantâneo, aumentar a frequência de atualização ou simplesmente reduzir os requisitos do canal.

Embalagem de bits

A primeira técnica é o empacotamento de bits. Consiste em utilizar exatamente a quantidade de bits necessária para descrever o valor desejado. Por exemplo, se você tiver um enum que pode ter 16 valores diferentes, em vez de um byte inteiro (8 bits), poderá usar apenas 4 bits.

Glenn Fiedler explica como implementar isso na segunda parte do artigo Lendo e escrevendo pacotes.

O empacotamento de bits funciona especialmente bem com amostragem, que será o tópico da próxima seção.

Amostragem

Amostragem é uma técnica de compactação com perdas que usa apenas um subconjunto de valores possíveis para codificar um valor. A maneira mais fácil de implementar a discretização é arredondando os números de ponto flutuante.

Glenn Fiedler (de novo!) mostra como colocar a amostragem em prática em seu artigo Compressão de instantâneo.

Algoritmos de compressão

A próxima técnica serão algoritmos de compressão sem perdas.

Aqui estão, na minha opinião, os três algoritmos mais interessantes que você precisa conhecer:

  • Codificação de Huffman com código pré-computado, que é extremamente rápido e pode produzir bons resultados. Foi usado para compactar pacotes no mecanismo de rede Quake3.
  • zlib é um algoritmo de compactação de uso geral que nunca aumenta a quantidade de dados. Como você pode ver aqui, ele tem sido usado em uma variedade de aplicações. Pode ser redundante para atualizar estados. Mas pode ser útil se você precisar enviar ativos, textos longos ou terrenos para clientes do servidor.
  • Copiando comprimentos de execução - Este é provavelmente o algoritmo de compressão mais simples, mas é muito eficaz para certos tipos de dados e pode ser usado como uma etapa de pré-processamento antes do zlib. É particularmente adequado para comprimir terrenos compostos por ladrilhos ou voxels nos quais muitos elementos adjacentes se repetem.

Compressão delta

A última técnica de compressão é a compressão delta. Consiste no fato de que apenas são transmitidas as diferenças entre o estado atual do jogo e o último estado recebido pelo cliente.

Foi usado pela primeira vez no mecanismo de rede Quake3. Aqui estão dois artigos explicando como usá-lo:

Glenn Fiedler também usou isso na segunda parte de seu artigo Compressão de instantâneo.

Шифрование

Além disso, pode ser necessário criptografar a transferência de informações entre clientes e o servidor. Há várias razões para isso:

  • privacidade/confidencialidade: as mensagens só podem ser lidas pelo destinatário e nenhuma outra pessoa que cheire a rede poderá lê-las.
  • autenticação: quem deseja desempenhar o papel de jogador deve conhecer sua chave.
  • Prevenção de cheats: Será muito mais difícil para jogadores mal-intencionados criarem seus próprios pacotes de cheats, eles terão que reproduzir o esquema de criptografia e encontrar a chave (que muda a cada conexão).

Eu recomendo fortemente usar uma biblioteca para isso. Eu sugiro usar libsódio, porque é especialmente simples e possui excelentes tutoriais. Particularmente interessante é o tutorial sobre troca de chaves, que permite gerar novas chaves a cada nova conexão.

Protocolo de Aplicação: Conclusão

Isso conclui nosso protocolo de aplicação. Acredito que a compressão é totalmente opcional e a decisão de utilizá-la depende apenas do jogo e da largura de banda necessária. A criptografia, na minha opinião, é obrigatória, mas no primeiro protótipo você pode passar sem ela.

Lógica de aplicação

Agora podemos atualizar o estado no cliente, mas podemos enfrentar problemas de latência. O jogador, após completar a entrada, precisa aguardar a atualização do estado do jogo no servidor para ver o impacto que isso teve no mundo.

Além disso, entre duas atualizações de estado, o mundo fica completamente estático. Se a taxa de atualização do estado for baixa, os movimentos serão muito bruscos.

Existem diversas técnicas para reduzir o impacto desse problema e irei abordá-las na próxima seção.

Técnicas de suavização de latência

Todas as técnicas descritas nesta seção são discutidas em detalhes na série Multijogador em ritmo acelerado Gabriel Gambeta. Recomendo fortemente a leitura desta excelente série de artigos. Também inclui uma demonstração interativa que permite ver como essas técnicas funcionam na prática.

A primeira técnica é aplicar o resultado de entrada diretamente, sem esperar por uma resposta do servidor. É chamado previsão do lado do cliente. Porém, quando o cliente recebe uma atualização do servidor, ele deve verificar se sua previsão estava correta. Caso contrário, ele só precisa mudar seu estado de acordo com o que recebeu do servidor, pois o servidor é autoritário. Esta técnica foi usada pela primeira vez em Quake. Você pode ler mais sobre isso no artigo Revisão de código do Quake Engine Fabien Sanglars [tradução em Habré].

O segundo conjunto de técnicas é usado para suavizar o movimento de outras entidades entre duas atualizações de estado. Existem duas maneiras de resolver este problema: interpolação e extrapolação. No caso de interpolação, são tomados os dois últimos estados e mostrada a transição de um para o outro. A desvantagem é que causa um pequeno atraso porque o cliente sempre vê o que aconteceu no passado. A extrapolação consiste em prever onde as entidades deveriam estar agora com base no último estado recebido pelo cliente. A sua desvantagem é que se a entidade mudar completamente a direção do movimento, haverá um grande erro entre a previsão e a posição real.

A técnica mais recente e avançada útil apenas em FPS é compensação de atraso. Ao usar a compensação de atraso, o servidor leva em consideração os atrasos do cliente ao atirar no alvo. Por exemplo, se um jogador deu um tiro na cabeça na tela, mas na realidade o alvo estava em um local diferente devido ao atraso, então seria injusto negar ao jogador o direito de matar devido ao atraso. Portanto, o servidor retrocede no tempo até o momento em que o jogador disparou para simular o que o jogador viu na tela e verificar se há colisão entre o tiro e o alvo.

Glenn Fiedler (como sempre!) escreveu um artigo em 2004 Física de Redes (2004), no qual ele lançou as bases para a sincronização de simulações físicas entre servidor e cliente. Em 2014 ele escreveu uma nova série de artigos Física de Redes, que descreveu outras técnicas para sincronizar simulações físicas.

Há também dois artigos no wiki da Valve, Fonte de rede multijogador и Métodos de compensação de latência no projeto e otimização de protocolos de jogo cliente/servidor que consideram a compensação por atrasos.

Prevenindo trapaças

Existem duas técnicas principais para prevenir a trapaça.

Primeiro: tornar mais difícil para os trapaceiros enviarem pacotes maliciosos. Conforme mencionado acima, uma boa maneira de implementar isso é a criptografia.

Segundo: um servidor autoritário deve receber apenas comandos/entradas/ações. O cliente não deve ser capaz de alterar o estado no servidor a não ser enviando entradas. Então, cada vez que o servidor recebe uma entrada, ele deve verificar se ela é válida antes de usá-la.

Lógica de aplicação: conclusão

Recomendo que você implemente uma forma de simular altas latências e baixas taxas de atualização para poder testar o comportamento do seu jogo em más condições, mesmo quando o cliente e o servidor estão rodando no mesmo computador. Isto simplificará enormemente a implementação de técnicas de suavização de atraso.

Outros recursos úteis

Se quiser explorar outros recursos sobre modelos de rede, você pode encontrá-los aqui:

Fonte: habr.com

Adicionar um comentário