Sobre o modelo de rede em jogos para iniciantes

imagem

Nas últimas duas semanas, tenho trabalhado em um mecanismo de rede para o meu jogo. Antes disso, eu não sabia nada sobre tecnologias de rede em jogos, então li muitos artigos e realizei muitos experimentos para entender todos os conceitos e poder escrever meu próprio mecanismo de rede.

Neste guia, gostaria de compartilhar com você 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. Na arquitetura ponto a ponto (p2p), os dados são transferidos entre qualquer par de players conectados e, na arquitetura cliente-servidor, os dados são transmitidos apenas entre os players e o servidor.

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

Em particular, estamos mais interessados ​​em servidores autoritários: nesses sistemas, o servidor está sempre certo. Por exemplo, se um jogador pensa que ele está em coordenadas (10, 5) e o servidor diz que ele está em (5, 3), o cliente deve substituir sua posição pela posição transmitida pelo servidor e não vice-versa. O uso de servidores autoritários facilita o reconhecimento de trapaceiros.

Existem três componentes principais nos sistemas de rede de jogos:

  • Protocolo de Transporte: como os dados são transferidos entre os clientes e o servidor.
  • Protocolo de aplicação: o que é transferido dos clientes para o servidor e do servidor para os clientes e em que formato.
  • Lógica do aplicativo: como os dados transmitidos são usados ​​para atualizar o status dos clientes e do servidor.

É muito importante entender o papel de cada parte e as dificuldades associadas.

Protocolo de transporte


O primeiro passo é escolher um protocolo para o transporte de dados entre o servidor e os clientes. Existem dois protocolos da Internet para isso: TCP e UDP . Mas você pode criar seu próprio protocolo de transporte com base em um deles ou usar a biblioteca na qual eles são usados.

Comparando TCP e UDP


O TCP e o UDP são baseados em IP . O IP permite transferir um pacote da origem para o destinatário, mas não garante que o pacote enviado chegue mais cedo ou mais tarde ao destinatário, que ele chegue ao menos uma vez e que a sequência de pacotes chegue na ordem correta. Além disso, um pacote pode conter apenas um tamanho de dados limitado especificado pelo valor da MTU .

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

Para entender por que essas funções podem causar um atraso, você precisa entender como o TCP funciona. Quando o nó de envio encaminha o pacote para o nó de recebimento, 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 perdida ou por outros motivos), ele reenviará o pacote. Além disso, o TCP garante que os pacotes sejam recebidos na ordem correta; portanto, até que um pacote perdido seja recebido, todos os outros pacotes não poderão ser processados, mesmo que já tenham sido recebidos pelo nó de recebimento.

Mas, como você provavelmente entende, o atraso nos jogos multiplayer é muito importante, especialmente em gêneros ativos como o FPS. É por isso que muitos jogos usam o 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, ele não se importa se o pacote não confiável chegou ao receptor. 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 fluxo para entrada do jogador e outro fluxo para mensagens de bate-papo. Se uma mensagem de bate-papo que não contém dados urgentes for perdida, ela não diminuirá a velocidade da entrada, o que é urgente. Ou, um protocolo proprietário pode implementar confiabilidade de maneira diferente da do TCP, a fim de ser mais eficiente nos videogames.

Então, se o TCP é uma porcaria, criaremos nosso próprio protocolo de transporte baseado no UDP?

Tudo é um pouco mais complicado. Embora o TCP seja quase subótimo para sistemas de rede de jogos, ele pode funcionar muito bem no seu jogo e economizar seu tempo valioso. Por exemplo, atraso pode não ser um problema para um jogo baseado em turnos ou um jogo que só pode ser jogado em LANs, onde há muito menos atrasos e perda de pacotes do que na Internet.

Muitos jogos de sucesso, incluindo World of Warcraft, Minecraft e Terraria, usam TCP. No entanto, a maioria dos FPS usa protocolos proprietários baseados em UDP, então falaremos mais sobre eles abaixo.

Se você decidir usar o TCP, verifique se o algoritmo Nagle está desativado, porque ele armazena em buffer os pacotes antes do envio, o que significa que aumenta o atraso.

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

Protocolo próprio


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

O primeiro artigo, Networking for Game Programmers 2008, é mais simples que o segundo, Building A Game Network Protocol 2016. Eu recomendo que você comece com um mais antigo.

Lembre-se de que Glenn Fiedler é um grande defensor do uso de seu próprio protocolo UDP. E depois de ler seus artigos, você certamente superará a opinião dele de que o TCP tem sérias desvantagens nos videogames e deseja implementar seu próprio protocolo.

Mas se você é novo na rede, faça um favor a si mesmo e use o TCP ou uma biblioteca. Para implementar com sucesso seu próprio protocolo de transporte, primeiro você precisa aprender muito.

Bibliotecas de rede


Se você precisar de algo mais eficiente que o TCP, mas não quiser se preocupar em implementar seu próprio protocolo e entrar em muitos detalhes, poderá usar a biblioteca de rede. Existem muitos deles:


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

Protocolo de Transporte: Conclusão


Para resumir: existem dois protocolos principais de transporte: TCP e UDP. O TCP possui muitos recursos úteis: confiabilidade, solicitação de pacotes, detecção de erros. O UDP não tem tudo isso, mas o TCP, por sua natureza, aumentou atrasos que são inaceitáveis ​​para alguns jogos. Ou seja, para garantir baixas latências, você pode criar seu próprio protocolo baseado em UDP ou usar uma biblioteca que implemente o protocolo de transporte UDP e seja adaptada para videogames para vários jogadores.

A escolha entre TCP, UDP e a biblioteca depende de vários fatores. Em primeiro lugar, pelas necessidades do jogo: ele precisa de baixas latências? Segundo, a partir dos requisitos do protocolo de aplicação: ele precisa de um protocolo confiável? Como veremos na próxima parte, você pode criar um protocolo de aplicativo para o qual um protocolo não confiável é bastante adequado. Por fim, você ainda precisa considerar a experiência do desenvolvedor do mecanismo de rede.

Eu tenho duas dicas:

  • Maximize o protocolo de transporte do restante do aplicativo para que ele possa ser facilmente substituído sem reescrever o código inteiro.
  • Não faça otimização prematura. Se você não é um especialista em rede e não tem certeza se precisa de seu próprio protocolo de transporte baseado em UDP, pode começar com o TCP ou uma biblioteca que fornece confiabilidade e, em seguida, testar e medir o desempenho. Se você tiver problemas e tiver certeza de que o motivo está no protocolo de transporte, talvez seja a hora de criar seu próprio protocolo de transporte.

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

Protocolo de aplicação


Agora que podemos trocar dados entre clientes e o servidor, precisamos decidir quais dados transferir e em qual 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 um estado completo, mas filtrado, com entidades próximas ao player. Ele faz isso por três razões. Primeiro, o estado geral pode ser muito grande para transmissão de 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 conhecer certos dados, por exemplo, a posição do oponente no outro extremo do mapa, porque, caso contrário, ele pode cheirar pacotes e saber exatamente para onde se mover para matá-lo.

Serialização


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

Imediatamente se pensa em usar um formato legível por humanos, como JSON ou XML. Mas será completamente ineficaz e em vão ocupará a maior parte do canal.

Em vez disso, é recomendável usar um formato binário muito mais compacto. Ou seja, os pacotes conterão apenas alguns bytes. Aqui você precisa considerar o problema da ordem dos bytes , que pode diferir em computadores diferentes.

Você pode usar uma biblioteca para serializar dados, por exemplo:


Apenas verifique se a biblioteca cria arquivos portáteis e cuida da ordem dos bytes.

Uma solução independente pode ser uma implementação independente, não é particularmente complicada, especialmente se você usar uma abordagem orientada a dados no código. Além disso, permitirá executar otimizações que nem sempre são possíveis ao usar a biblioteca.

Glenn Fiedler escreveu dois artigos sobre serialização: ler e escrever pacotes e estratégias de serialização .

Compressão


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

Bit embalagem


A primeira técnica é a embalagem de bits. Consiste em usar exatamente o número de bits necessário para descrever o valor desejado. Por exemplo, se você tiver uma enumeração que possa 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 Pacotes de leitura e gravação .

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

Discretização


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

Glenn Fiedler (novamente!) Mostra como aplicar a amostragem na prática em seu artigo Snapshot Compression .

Algoritmos de compressão


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

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

  • Codificação de Huffman com código pré-calculado que é extremamente rápido e pode dar bons resultados. Foi usado para compactar pacotes no mecanismo de rede Quake3.
  • O zlib é um algoritmo de compactação de uso geral que nunca aumenta a quantidade de dados. Como pode ser visto aqui , ele tem sido usado em muitas aplicações. Pode ser redundante para atualizar estados. Mas pode ser útil se você precisar enviar ativos, textos longos ou alívio para os clientes a partir do servidor.
  • A cópia de séries é provavelmente o algoritmo de compactação mais simples, mas é muito eficaz para determinados tipos de dados e pode ser usado como uma etapa de pré-processamento antes do zlib. É especialmente adequado para comprimir terrenos constituídos por ladrilhos ou voxels, nos quais muitos elementos vizinhos são repetidos.

Compressão delta


A técnica de compactação mais recente é a compactação delta. Está no fato de que apenas diferenças entre o estado atual do jogo e o último estado recebido pelo cliente são transmitidas.

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


Glenn Fiedler também o usou na segunda parte de seu artigo sobre Snapshot Compression .

Criptografia


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

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

Eu recomendo fortemente o uso da biblioteca para isso. Eu sugiro usar libsodium porque é especialmente simples e tem ótimos tutoriais. De particular interesse é o tutorial de troca de chaves , que permite gerar novas chaves a cada nova conexão.

Protocolo de Aplicação: Conclusão


Terminaremos com o protocolo do aplicativo. Acredito que a compactação é completamente opcional e a decisão de usá-la depende apenas do jogo e da largura de banda necessária. Criptografia, na minha opinião, é obrigatória, mas no primeiro protótipo você pode ficar sem ela.

Lógica de aplicação


Agora, podemos atualizar o estado no cliente, mas podemos encontrar problemas com atrasos. Depois de entrar, o jogador precisa aguardar a atualização do estado do jogo do servidor para ver qual o impacto que ele teve no mundo.

Além disso, entre duas atualizações de estado, o mundo é completamente estático. Se a taxa de atualização dos estados for baixa, os movimentos ficarão muito trêmulos.

Existem várias técnicas para reduzir o impacto desse problema e, na próxima seção, falarei sobre elas.

Atraso nas técnicas de suavização


Todas as técnicas descritas nesta seção são discutidas em detalhes na série Multiplayer em ritmo acelerado, de Gabriel Gambetta. Eu recomendo a leitura desta grande série de artigos. Ele também possui uma demonstração interativa que permite ver como essas técnicas funcionam na prática.

A primeira técnica é aplicar a entrada diretamente, sem aguardar uma resposta do servidor. Isso é chamado de previsão do lado do cliente . No entanto, quando o cliente recebe a atualização do servidor, ele deve verificar se sua previsão estava correta. Se não for assim, ele só precisará alterar seu estado de acordo com o recebido do servidor, porque o servidor é autoritário. Esta técnica foi usada pela primeira vez no Quake. Você pode ler mais sobre isso no artigo Revisão do código do Quake Engine por Fabien Sanglar [ tradução para Habré].

O segundo conjunto de técnicas é usado para facilitar a movimentação de outras entidades entre duas atualizações de estado. Existem duas maneiras de resolver esse problema: interpolação e extrapolação. No caso de interpolação, os dois últimos estados são tomados e a transição de um para o outro é mostrada. Sua desvantagem é que causa uma pequena fração do atraso, porque o cliente sempre vê o que aconteceu no passado. A extrapolação está prevendo onde as entidades agora devem se basear no último estado recebido pelo cliente. 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 última técnica mais avançada, útil apenas no FPS é a compensação de atraso .Ao usar a compensação de atraso, o servidor leva em consideração os atrasos do cliente quando dispara em um alvo. Por exemplo, se um jogador completou um tiro na cabeça em sua tela, mas, na realidade, seu objetivo foi devido a um atraso em outro lugar, seria desonesto negar a um jogador o direito de matar por causa de um atraso. Portanto, o servidor retrocede o tempo até o momento em que o jogador disparou para simular o que o jogador viu na tela e verificar a colisão entre o tiro e o alvo.

Glenn Fiedler (como sempre!) Escreveu um artigo de 2004 na Network Physics (2004) , que lançou as bases para sincronizar simulações de física entre um servidor e um cliente. Em 2014, ele escreveu uma nova série de artigos de Networking Physics que descreviam outras técnicas para sincronizar simulações de física.

Existem também dois artigos no wiki da Valve, Rede de Multijogadores de Origem e Métodos de Compensação de Latência no Projeto e Otimização de Protocolo no Jogo Cliente / Servidor , que discutem a compensação de atraso.

Prevenção de trapaça


Existem duas técnicas principais para evitar trapaças.

Primeiro: complicar o envio de pacotes maliciosos por trapaceiros. Como mencionado acima, a criptografia é uma boa maneira de implementá-la.

Segundo: um servidor autoritário deve receber apenas comandos / entradas / ações. O cliente não deve poder alterar o estado no servidor, exceto enviando entrada. Então, toda vez que a entrada é recebida, o servidor deve verificar sua validade antes de aplicá-la.

Lógica de Aplicação: Conclusão


Eu recomendo que você implemente um método de simular grandes atrasos e baixas taxas de atualização para poder testar o comportamento do seu jogo em condições precárias, mesmo quando o cliente e o servidor estiverem executando no mesmo computador. Isso simplificará bastante a implementação de técnicas de suavização de retardo.

Outros recursos úteis


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

Source: https://habr.com/ru/post/pt467025/


All Articles