
Em maio deste ano, eu participei como jogador de . Notei que, quando o número de jogadores atinge um certo limite, a cada poucos minutos, parte deles "cai". Felizmente para vocês (mas não para mim), eu fui um dos jogadores que desconectavam todas as vezes, mesmo com uma boa conexão. Eu encarei isso como um desafio pessoal e comecei a procurar as causas do problema. Após três semanas de depuração, testes e correções, o erro foi finalmente resolvido, mas essa jornada não foi tão simples.
Os problemas em jogos multiplayer são muito difíceis de rastrear. Geralmente, eles ocorrem em condições de rede muito específicas e em situações de jogo bastante específicas (neste caso, a presença de mais de 200 jogadores). E mesmo quando é possível reproduzir o problema, não é fácil depurá-lo adequadamente, porque a inserção de pontos de controle interrompe o jogo, confunde os temporizadores e geralmente leva a uma desconexão devido a tempo limite. Mas, graças à persistência e a uma ferramenta incrível chamada , consegui descobrir o que está acontecendo.
Resumindo: devido a um erro e à implementação incompleta da simulação do estado de latência, o cliente às vezes se encontrava em uma situação onde precisava enviar, em um único ciclo, um pacote de rede composto por ações de entrada do jogador envolvendo cerca de 400 entidades de jogo (nós chamamos isso de "megapacote"). Após isso, o servidor não só precisava receber corretamente todas essas ações de entrada, mas também enviá-las para todos os outros clientes. Se você tem 200 clientes, isso rapidamente se torna um problema. O canal para o servidor rapidamente se entope, resultando em perda de pacotes e um efeito cascata de pacotes solicitados novamente. O atraso nas ações de entrada leva ainda mais clientes a começar a enviar megapacotes, aumentando ainda mais a avalanche. Os clientes sortudos conseguem se recuperar, enquanto todos os outros "caem".

O problema era bastante fundamental e levei 2 semanas para resolvê-lo. É bastante técnico, então abaixo explicarei os detalhes técnicos complexos. Mas, para começar, você precisa saber que desde a versão 0.17.54, lançada em 4 de junho, a conectividade multiplayer se tornou mais estável em condições de problemas temporais, e a ocultação de latências — muito menos bugada (menos engasgos e teletransportes). Além disso, mudei a forma de ocultar latências em combate e espero que, com isso, elas sejam um pouco mais suaves.
Multiplayer Mega Pack — detalhes técnicos
Simplificando, o multiplayer do jogo funciona da seguinte maneira: todos os clientes simulam o estado do jogo, recebendo e enviando apenas a entrada do jogador (chamada de "ações de entrada", Input Actions). A principal tarefa do servidor é transmitir Input Actions e controlar que todos os clientes realizem as mesmas ações em um ciclo. Mais detalhes podem ser lidos no post .
Como o servidor deve decidir quais ações executar, a ação do jogador percorre aproximadamente este caminho: ação do jogador -> cliente do jogo -> rede -> servidor -> rede -> cliente do jogo. Isso significa que cada ação do jogador é executada apenas após fazer a viagem de ida e volta pela rede. Devido a isso, o jogo pareceria extremamente lento, portanto, quase imediatamente após a introdução do multiplayer, foi implementado um mecanismo de ocultação de latências. A ocultação de latências simula a entrada do jogador sem considerar as ações de outros jogadores e a tomada de decisões do servidor.

No Factorio, existe o estado do jogo Game State — é o estado completo do mapa, do jogador, das entidades e de tudo mais. Ele é simulado de forma determinística em todos os clientes com base nas ações recebidas do servidor. O estado do jogo é sagrado e, se alguma vez começar a diferir do servidor ou de qualquer outro cliente, ocorrerá uma desincronização.
Além de Game State temos o estado de latências Latency State. Ele contém um pequeno subconjunto do estado principal. Latency State não é sagrado e simplesmente representa uma imagem de como o estado do jogo parecerá no futuro com base nas entradas do jogador. Input Actions.
Para isso, mantemos uma cópia das ações criadas Input Actions na fila de latências.

Ou seja, no final do processo, do lado do cliente, a imagem se apresenta aproximadamente assim:
- Aplicamos Input Actions todos os jogadores a Game State de acordo com as ações de entrada que foram recebidas do servidor.
- Removemos da fila de atrasos todos Input Actions, que, segundo o servidor, já foram aplicados a Game State.
- Removemos Latency State e o redefinimos para que ele apareça exatamente como Game State.
- Aplicamos todas as ações da fila de atrasos a Latency State.
- Com base nos dados Game State e Latency State renderizamos o jogo para o jogador.
Tudo isso se repete em cada ciclo.
Muito complicado? Não se preocupe, ainda não acabou. Para compensar a instabilidade das conexões de internet, criamos dois mecanismos:
- Ciclos perdidos: quando o servidor decide que Input Actions serão executados no ciclo do jogo, se ele não recebeu Input Actions um determinado jogador (por exemplo, devido ao aumento da latência), ele não esperará, mas informará a esse cliente "não considerei suas Input Actions, tentarei adicioná-los ao próximo ciclo". Isso é feito para que problemas de conexão (ou do computador) de um jogador não desacelerem a atualização do mapa para todos os outros. Vale notar que Input Actions não são ignorados, mas apenas adiados.
- Latência do caminho total de ida e volta: o servidor tenta suprimir qual é a latência de transmissão de dados de ida e volta entre o cliente e o servidor para cada cliente. A cada 5 segundos, ele discute, se necessário, com o cliente uma nova latência (dependendo de como a conexão se comportou no passado), e aumenta ou diminui a latência de transmissão de dados de ida e volta, conforme apropriado.
Esses mecanismos, por si só, são bastante simples, mas quando usados em conjunto (o que frequentemente acontece em problemas de conexão), a lógica do código se torna difícil de gerenciar e cheia de casos limite. Além disso, quando esses mecanismos estão envolvidos, o servidor e a fila de atrasos devem implementar corretamente um Input Action chamada StopMovementInTheNextTick. Graças a isso, em caso de problemas de conexão, o personagem não se moverá sozinho (por exemplo, para debaixo de um trem).
Agora preciso explicar como funciona a seleção de entidades. Um dos tipos transmitidos Input Action — é uma mudança no estado da seleção da entidade. Ela informa a todos sobre qual entidade o jogador passou o mouse. Como você pode entender, esta é uma das ações de entrada mais frequentes enviadas pelos clientes, por isso, para economizar a largura de banda, a otimizamos para que ocupe o menor espaço possível. Isso é realizado da seguinte forma: ao selecionar cada entidade, em vez de salvar coordenadas absolutas e de alta precisão do mapa, o jogo salva um deslocamento relativo de baixa precisão em relação à seleção anterior. Isso funciona bem, pois a seleção com o mouse geralmente ocorre muito próximo da seleção anterior. Devido a isso, surgem duas exigências importantes: Input Actions nunca podem ser negligenciadas e devem ser realizadas na ordem correta. Essas exigências são atendidas para Game State. Mas como a tarefa Estado de latência é "aparentar estar bom o suficiente" para o jogador, em condições de latência, elas não são atendidas. Latency State não considera , relacionados com o atraso de ciclos e a variação de latência na comunicação de ida e volta.
Você já pode imaginar para onde tudo isso está indo. Finalmente, começamos a ver as causas do problema do megapacote. A raiz do problema é que, ao decidir se o envio do evento de mudança de seleção é necessário, a lógica de seleção de entidades depende de Latency State, e esse estado nem sempre contém informações corretas. Portanto, o megapacote é gerado mais ou menos assim:
- O jogador está enfrentando problemas de conexão.
- Mecanismos de atraso de ciclo e regulação de latência de ida e volta entram em cena.
- A fila de estados de latência não considera esses mecanismos. Isso resulta na exclusão prematura de algumas ações ou na execução em uma ordem incorreta, levando a um comportamento incorreto. Latency State.
- O jogador perdeu o problema de conexão e, para se sincronizar com o servidor, simula até 400 ciclos.
- A cada ciclo, uma nova ação de mudança de seleção de entidade é gerada e preparada para ser enviada ao servidor.
- O cliente envia ao servidor um megapacote com mais de 400 mudanças de seleção de entidades (e outras ações: o estado de tiro, de caminhada, etc. também sofreram com esse problema).
- O servidor recebe 400 ações de entrada. Como não é permitido ignorar nenhuma ação de entrada, ele exige que todos os clientes realizem essas ações e as envie pela rede.
A ironia é que o mecanismo destinado a economizar largura de banda acabou criando pacotes de rede enormes.
Resolvemos esse problema corrigindo todos os casos limites de atualização e apoio à fila de atrasos. Embora isso tenha levado um bom tempo, no final, valeu a pena implementar tudo corretamente, em vez de confiar em soluções rápidas.
Fonte: habr.com
