
Nas últimas duas semanas, trabalhei em um motor de rede para meu jogo. Antes disso, eu não sabia nada sobre tecnologias de rede em jogos, então li muitos artigos e fiz vários experimentos para entender todos os conceitos e ser capaz de escrever meu próprio motor de rede.
Neste guia, gostaria de compartilhar com você diferentes conceitos que você precisa estudar antes de escrever seu próprio motor de jogo, assim como os melhores recursos e artigos para aprendê-los.
Em geral, existem dois principais tipos de arquiteturas de rede: peer-to-peer e cliente-servidor. Na arquitetura peer-to-peer (p2p), os dados são transmitidos entre quaisquer pares de jogadores conectados, enquanto na arquitetura cliente-servidor, os dados são transmitidos apenas entre jogadores e o servidor.
Embora a arquitetura peer-to-peer ainda seja utilizada em alguns jogos, o padrão é a arquitetura cliente-servidor: ela é mais fácil de implementar, requer uma largura de banda mais baixa e facilita a proteção contra trapaças. Portanto, neste guia, vamos nos concentrar na arquitetura cliente-servidor.
Em particular, estamos mais interessados em servidores autoritários: em tais sistemas, o servidor sempre está certo. Por exemplo, se um jogador acha que está nas coordenadas (10, 5), mas o servidor diz que ele está em (5, 3), o cliente deve substituir sua posição pela que o servidor está passando, e não o contrário. O uso de servidores autoritários simplifica o reconhecimento de trapaceiros.
Em sistemas de rede de jogos, existem três componentes principais:
- Protocolo de transporte: como os dados são transmitidos entre clientes e o 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 transmitidos são usados para atualizar o estado dos clientes e do servidor.
É muito importante entender o papel de cada parte e as dificuldades associadas a elas.
Protocolo de transporte
O primeiro passo é escolher um protocolo para transportar dados entre o servidor e os clientes. Existem dois protocolos de Internet para isso: e . Mas você pode criar seu próprio protocolo de transporte baseado em um deles ou usar uma biblioteca que tenha os dois.
Comparação entre TCP e UDP
Tanto o TCP quanto o UDP são baseados em O IP permite a transmissão de pacotes do remetente ao destinatário, mas não garante que o pacote enviado chegará ao destinatário, nem que chegará pelo menos uma vez, nem que a sequência dos pacotes será recebida na ordem correta. Além disso, um pacote pode conter apenas um tamanho limitado de dados, determinado pelo valor. .
O UDP é apenas uma camada delgada sobre o IP. Portanto, possui as mesmas limitações. Em contraste, o TCP possui muitas características distintas. Ele garante uma conexão confiável e ordenada entre dois nós com verificação de erros. Assim, o TCP é muito conveniente e é utilizado em muitos outros protocolos, como em. , e Mas todas essas funções têm um preço: .
Para entender por que essas funções podem causar atraso, é preciso entender como o TCP funciona. Quando o nó remetente envia um pacote ao nó destinatário, ele espera receber uma confirmação (ACK). Se, após um certo tempo, não a recebe (porque o pacote ou a confirmação foi perdida, ou por outros motivos), ele reenvia o pacote. Além disso, o TCP garante a recepção dos pacotes na ordem correta, então, enquanto um pacote perdido não for recebido, todos os outros pacotes não podem ser processados, mesmo que já tenham sido recebidos pelo nó destinatário.
Mas, como você deve entender, o atraso em jogos multijogador é muito importante, especialmente em gêneros ativos, como FPS. É por isso que muitos jogos usam UDP com seu próprio protocolo.
Um protocolo próprio baseado em UDP pode ser mais eficiente que o TCP por várias razões. Por exemplo, ele pode marcar alguns pacotes como confiáveis e outros como não confiáveis. Portanto, não se preocupa se o pacote não confiável chegou ao destinatário. Ou pode lidar com múltiplos fluxos de dados, para que um pacote perdido em um fluxo não atrase os outros fluxos. Por exemplo, pode haver um fluxo para a entrada do jogador e outro fluxo para mensagens de chat. Se uma mensagem de chat, que não é um dado urgente, for perdida, isso não atrasará a entrada, que é urgente. Ou o protocolo próprio pode implementar a confiabilidade de maneira diferente da do TCP, para ser mais eficiente em condições de videogame.
Então, se o TCP é tão ruim, vamos criar nosso próprio protocolo de transporte baseado em UDP?
Tudo é um pouco mais complicado. Mesmo que o TCP seja quase subótimo para sistemas de rede de jogos, ele pode funcionar muito bem no seu jogo específico e economizar seu precioso tempo. Por exemplo, a latência pode não ser um problema para um jogo por turnos ou um jogo que só pode ser jogado em redes LAN, onde a latência e a perda de pacotes são muito menores do que na internet.
Em muitos jogos de sucesso, incluindo World of Warcraft, Minecraft e Terraria, o TCP é utilizado. No entanto, na maioria dos FPS, são utilizados protocolos próprios baseados em UDP, portanto, abaixo discutiremos isso em mais detalhes.
Se você decidir usar TCP, certifique-se de que está desativado , pois ele faz o buffer dos pacotes antes de enviá-los, o que aumenta a latência.
Para saber mais sobre as diferenças entre UDP e TCP no contexto de jogos multiplayer, você pode ler o artigo de Glenn Fidler .
Protocolo próprio
Então, você quer criar seu próprio protocolo de transporte, mas não sabe por onde começar? Você teve sorte, pois Glenn Fidler escreveu dois artigos incríveis sobre isso. Neles, você encontrará muitas ideias inteligentes.
O primeiro artigo, de 2008, é mais simples do que o segundo, de 2016. Eu recomendo que você comece pelo mais antigo.
Tenha em mente que Glenn Fidler é um grande defensor do uso de um protocolo próprio baseado em UDP. E, depois de ler seus artigos, você com certeza adotará a opinião dele de que o TCP tem sérias desvantagens em videogames e quererá implementar seu próprio protocolo.
Mas se você é novo em redes, faça um favor a si mesmo e use TCP ou uma biblioteca. Para implementar com sucesso seu próprio protocolo de transporte, você precisa aprender muitas coisas primeiro.
Bibliotecas de rede
Se você precisa de algo mais eficiente do que 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 muitas delas:
- de Glenn Fidler
- , que não é mais mantida, mas seu fork parece que ainda está ativo.
- é uma biblioteca criada para FPS multiplayer
- da Valve
Eu não experimentei todos, mas eu prefiro a ENet porque é fácil de usar e confiável. Além disso, tem documentação clara e tutoriais para iniciantes.
Protocolo de transporte: conclusão
Em resumo: existem dois protocolos de transporte principais: TCP e UDP. O TCP possui muitas características úteis: confiabilidade, preservação da ordem dos pacotes, detecção de erros. O UDP não tem nada disso, mas o TCP, por sua natureza, tem atrasos mais elevados, inaceitáveis para alguns jogos. Portanto, para garantir baixos atrasos, pode-se criar um protocolo próprio baseado em UDP ou usar uma biblioteca que implemente um protocolo de transporte sobre UDP e adaptada para jogos multiplayer.
A escolha entre TCP, UDP e uma biblioteca depende de vários fatores. Primeiro, das necessidades do jogo: ele precisa de baixos atrasos? Em segundo lugar, das exigências do aplicativo: ele precisa de um protocolo confiável? Como veremos na próxima parte, é possível criar um protocolo de aplicativo que se adapte bem a um protocolo não confiável. Finalmente, é preciso também considerar a experiência do desenvolvedor do motor de rede.
Eu tenho duas dicas:
- Abstraia ao máximo o protocolo de transporte do resto do aplicativo, para que ele possa ser facilmente substituído sem reescrever todo o código.
- Não faça otimizações prematuras. Se você não é um especialista em redes e não tem certeza se precisa de um protocolo de transporte próprio baseado em UDP, comece com TCP ou uma biblioteca que fornece confiabilidade e depois teste e meça o desempenho. Se houver problemas e você estiver certo de que a causa está no protocolo de transporte, talvez seja a hora de criar seu próprio protocolo.
Para concluir esta parte, recomendo que você leia de Brian Hook, que aborda muitos dos tópicos discutidos aqui.
Protocolo de aplicativo
Agora que podemos trocar dados entre clientes e servidores, precisamos decidir quais dados transmitir e em qual formato.
O esquema clássico é que os clientes enviam ao servidor entradas ou ações, e o servidor envia aos clientes o estado atual do jogo.
O servidor envia um estado não completo, mas filtrado, com entidades que estão próximas ao jogador. Ele faz isso por três razões. Primeiro, o estado completo pode ser grande demais para ser transmitido com alta frequência. Segundo, os clientes estão principalmente interessados em dados visuais e de áudio, pois a maior parte da lógica do jogo é simulada no servidor do jogo. Por último, em alguns jogos, o jogador não deve saber determinados dados, como a posição do inimigo do outro lado do mapa, pois, se souber, ele pode sniffar os pacotes e saber exatamente para onde ir para matá-lo.
Serialização
O primeiro passo será transformar os dados que queremos enviar (entrada ou estado do jogo) em um formato adequado para transmissão. Esse processo é chamado de .
Vem à mente a ideia de usar um formato legível, como JSON ou XML. Mas isso seria completamente ineficiente e ocuparia a maior parte da largura de banda de forma desnecessária.
Em vez disso, é recomendado usar um formato binário, que é muito mais compacto. Ou seja, os pacotes conterão apenas alguns bytes. Aqui precisamos considerar o problema do , que pode diferir entre computadores.
Para a serialização de dados, é possível usar uma biblioteca, como:
- da Google
- da Sandstorm
- de Shane Grant e Randolph Vorhies
Apenas certifique-se de que a biblioteca cria arquivos portáteis e cuida da ordem dos bytes.
Uma solução alternativa pode ser a implementação própria, que não é muito complexa, especialmente se você usar uma abordagem orientada a dados no código. Além disso, isso permitirá que você faça otimizações que nem sempre são possíveis ao usar uma biblioteca.
Glenn Fidler escreveu dois artigos sobre serialização: e .
Compressão
A quantidade de dados transmitidos entre clientes e servidores é limitada pela largura de banda do canal. A compressão de dados permitirá enviar mais informações em cada instantâneo, aumentar a frequência de atualização ou simplesmente reduzir as exigências do canal.
Empacotamento de bits
A primeira técnica é a compactação bit a bit. Consiste em usar exatamente a quantidade de bits necessária para descrever o valor desejado. Por exemplo, se você tem uma enumeração que pode ter 16 valores diferentes, em vez de usar um byte inteiro (8 bits), você pode usar apenas 4 bits.
Glenn Fidler explica como implementar isso na segunda parte do artigo. .
A compactação bit a bit funciona especialmente bem com a discretização, que será o tema da próxima seção.
Discretização
é uma técnica de compressão com perda que envolve o uso de apenas um subconjunto dos valores possíveis para a codificação de um valor. A forma mais simples de implementar a discretização é arredondando números de ponto flutuante.
Glenn Fidler (de novo!) mostra como aplicar a discretização na prática em seu artigo. .
Algoritmos de compressão
A próxima técnica será algoritmos de compressão sem perdas.
Aqui estão, na minha opinião, três dos algoritmos mais interessantes que você precisa conhecer:
- com código pré-calculado, que é extremamente rápido e pode oferecer bons resultados. Ele foi usado para compactação de pacotes no mecanismo de rede Quake3.
- é um algoritmo de compressão de propósito geral que nunca aumenta o volume dos dados. Como pode ser visto, ele foi aplicado em muitas áreas de aplicação. Para atualização de estados, pode ser redundante. Mas pode ser útil se você precisar enviar ativos, textos longos ou relevo aos clientes a partir do servidor.
- é 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 a compressão de relevo composto por tiles ou voxels, onde muitos elementos vizinhos se repetem.
Compressão Delta
A última metodologia de compressão é a compressão delta. Ela envolve a transmissão apenas das diferenças entre o estado atual do jogo e o último estado recebido pelo cliente.
Ela foi aplicada pela primeira vez no mecanismo de rede Quake3. Aqui estão dois artigos que explicam como usá-la:
- de Bryan Hook
- de Fabien Sanglard [ artigos no Habr, veja a seção "Modelo de Rede" ]
Glenn Fiddler também a utilizou na segunda parte de seu artigo .
Criptografia
Além disso, você pode precisar criptografar a transmissão de informações entre clientes e servidores. Existem várias razões para isso:
- privacidade/confidencialidade: as mensagens só podem ser lidas pelo destinatário, e nenhuma outra pessoa que esteja realizando sniffing na rede conseguirá lê-las.
- autenticação: a pessoa que deseja atuar como jogador deve conhecer sua chave.
- prevenção de trapaça: jogadores mal-intencionados terão muito mais dificuldade em criar seus próprios pacotes para trapaça, pois terão que reproduzir o esquema de criptografia e encontrar a chave (que muda a cada conexão).
Recomendo enfaticamente usar uma biblioteca para isso. Sugiro usar , pois é especialmente simples e possui ótimos tutoriais. O tutorial sobre , que permite gerar novas chaves a cada nova conexão.
Protocolo de Aplicação: Conclusão
Com isso, terminamos com o protocolo de aplicação. Acredito que a compressão seja completamente desnecessária e a decisão de usá-la depende apenas do jogo e da largura de banda necessária. A criptografia, na minha opinião, é obrigatória, mas na primeira versão do protótipo, pode-se dispensar.
Lógica da Aplicação
Agora podemos atualizar o estado no cliente, mas podemos enfrentar problemas de latência. Um jogador, ao executar uma entrada, precisa esperar pela atualização do estado do jogo do servidor para ver qual impacto teve no mundo.
Além disso, entre duas atualizações de estado, o mundo é completamente estático. Se a frequência de atualização de estados for baixa, os movimentos serão muito abruptos.
Existem várias técnicas que permitem reduzir o impacto desse problema, e na próxima seção falarei sobre elas.
Técnicas de Suavização de Latência
Todas as técnicas descritas nesta seção são discutidas em detalhes na série de Gabriel Gambetta. Recomendo enfaticamente a leitura dessa excelente série de artigos. Também há uma demonstração interativa que permite ver como essas técnicas funcionam na prática.
A primeira técnica é aplicar o resultado da entrada diretamente, sem esperar pela resposta do servidor. Isso é chamado de previsão no lado do cliente. No entanto, quando o cliente recebe uma atualização do servidor, ele deve garantir que sua previsão estava correta. Se não estiver, ele apenas precisa alterar seu estado de acordo com o que foi recebido do servidor, pois o servidor é autoritário. Essa técnica foi usada pela primeira vez no Quake. Você pode ler mais sobre isso no artigo de Fabien Sanglard [ no Habr].
Um segundo conjunto de técnicas é utilizado para suavizar o movimento de outras entidades entre duas atualizações de estado. Existem duas maneiras de resolver essa tarefa: interpolação e extrapolação. No caso da interpolação, os dois últimos estados são considerados, mostrando a transição de um para o outro. Sua desvantagem é que causa um pequeno grau de atraso, pois o cliente sempre vê o que aconteceu no passado. A extrapolação, por outro lado, envolve prever onde as entidades deveriam estar agora com base no último estado recebido pelo cliente. Sua desvantagem é que, se uma entidade mudar completamente a direção do movimento, haverá uma grande discrepância entre a previsão e a posição real.
A última e mais avançada técnica, útil apenas em FPS, é compensação de lag. Ao usar a compensação de lag, o servidor considera os atrasos do cliente quando ele atira em um alvo. Por exemplo, se um jogador fez um headshot na sua tela, mas, na realidade, seu alvo estava em outro lugar devido ao atraso, seria injusto negar ao jogador o direito ao kill devido ao lag. Portanto, o servidor retrocede no tempo até o momento em que o jogador atirou, para simular o que o jogador viu na sua tela e verificar a colisão entre o seu tiro e o alvo.
Glenn Fiddler (como sempre!) escreveu em 2004 um artigo , no qual lançou as bases para a sincronização da simulação de física entre o servidor e o cliente. Em 2014, ele escreveu uma nova série de artigos , que descreveu outras técnicas para sincronizar a simulação de física.
Além disso, na wiki da Valve, há dois artigos, e , que discutem a compensação de atrasos.
Prevenção de trapaças
Existem duas técnicas principais de prevenção de trapaças.
Primeiro: complicação no envio de pacotes maliciosos pelos hackers. Como mencionado acima, uma boa forma de implementá-la é pela criptografia.
Segundo: o servidor autoritário deve receber apenas comandos / entrada / ações. O cliente não deve ter a capacidade de alterar o estado no servidor, exceto enviando uma entrada. Assim, o servidor deve verificar a validade da entrada sempre que receber um comando antes de aplicá-lo.
Lógica do aplicativo: conclusão
Recomendo que você implemente um método de simulação de grandes atrasos e baixas taxas de atualização, para que possa testar o comportamento do seu jogo em condições adversas, mesmo quando o cliente e o servidor estão rodando no mesmo computador. Isso facilitará muito a implementação de técnicas de suavização de latência.
Outros recursos úteis
Se você deseja explorar outros recursos dedicados a modelos de rede, pode encontrá-los aqui:
- — vale a pena ler todo o seu blog, que contém muitos ótimos artigos. todas as publicações sobre tecnologias de rede estão reunidas.
- do autor M. Fatih MAR — é uma lista detalhada de artigos e vídeos sobre motores de rede de videogames.
- Em também possui muitos links úteis.
Fonte: habr.com
