Megapack: como os desenvolvedores do Factorio conseguiram resolver o problema com o multiplayer de 200 jogadores

imagem

Em maio deste ano, participei como jogador do evento MMO KatherineOfSky . Percebi que quando o número de jogadores atinge um certo número, a cada poucos minutos alguns deles "caem". Felizmente para você (mas não para mim), eu fui um daqueles jogadores que se desconectavam todas as vezes , mesmo com uma boa conexão. Tomei isso como um desafio pessoal e comecei a procurar as causas do problema. Após três semanas de depuração, teste e correção, o erro foi finalmente corrigido, mas essa jornada não era tão simples.

Os problemas dos jogos multiplayer são muito difíceis de localizar. Geralmente eles surgem em condições muito específicas de parâmetros de rede e em condições muito específicas do jogo (neste caso, a presença de mais de 200 jogadores). E mesmo quando é possível reproduzir o problema, ele não pode ser depurado adequadamente, porque a inserção de pontos de controle interrompe o jogo, confunde os temporizadores e geralmente leva ao encerramento da conexão devido ao excesso de tempo de espera. Mas, graças à teimosia e à maravilhosa ferramenta chamada desajeitada, consegui descobrir o que estava acontecendo.

Resumindo: devido a um erro e à implementação incompleta da simulação do estado de atraso, o cliente às vezes se encontra em uma situação em que ele precisa enviar um pacote de rede em um ciclo de clock, consistindo nas ações do jogador para selecionar cerca de 400 entidades do jogo (chamamos de "megapackage"). Depois disso, o servidor não deve apenas receber corretamente todas essas ações de entrada, mas também enviá-las a todos os outros clientes. Se você tem 200 clientes, isso rapidamente se torna um problema. O canal para o servidor é rapidamente entupido, o que leva à perda de pacotes e a uma cascata de pacotes solicitados novamente. Adiar as ações de entrada leva ao fato de que ainda mais clientes começam a enviar megafones, e sua avalanche se torna ainda mais forte. Clientes bem-sucedidos conseguem se recuperar, todo o resto "cai".

imagem

O problema era bastante fundamental e levei duas semanas para corrigi-lo. É bastante técnico, então, a seguir, explicarei os detalhes técnicos. Mas primeiro você precisa saber que desde a versão 0.17.54, lançada em 4 de junho, diante de problemas temporários de conexão, o multiplayer se tornou mais estável e a ocultação de atrasos é muito menos complicada (menos frenagem e teletransporte). Além disso, mudei a maneira de ocultar os atrasos nos combates e espero que, graças a isso, eles sejam um pouco mais suaves.

Megapack para múltiplos usuários - Detalhes técnicos


Em termos simples, o multiplayer no jogo funciona da seguinte maneira: todos os clientes simulam o estado do jogo, recebendo e enviando apenas a entrada do jogador (chamada "Ações de entrada", Ações de entrada ). A principal tarefa do servidor é transmitir ações de entrada e controlar que todos os clientes executem as mesmas ações em um ciclo. Leia mais sobre isso no post FFF-149 .

Como o servidor deve tomar decisões sobre quais ações executar, as ações do jogador se movem por esse caminho: ação do jogador -> cliente do jogo -> rede -> servidor -> rede -> cliente do jogo. Isso significa que a ação de cada jogador é executada somente depois que ele faz um caminho de ida e volta pela rede. Por causa disso, o jogo pareceria terrivelmente lento, então quase imediatamente após o multiplayer aparecer no jogo, um mecanismo para ocultar atrasos foi introduzido. Ocultar um atraso imita a contribuição de um jogador sem levar em consideração as ações de outros jogadores e as decisões do servidor.


O Factorio possui um estado de jogo chamado Game State - este é o estado completo do mapa, jogador, entidades e tudo mais. É simulado de forma determinística em todos os clientes com base nas ações recebidas do servidor. O estado do jogo é sagrado e, se algum dia começar a diferir do servidor ou de qualquer outro cliente, ocorrerá a dessincronização.

Além do estado do jogo , temos um estado de latência do estado de latência. Ele contém um pequeno subconjunto do estado fundamental. O estado de latência não é sagrado e simplesmente apresenta uma imagem de como o estado do jogo será no futuro, com base nas ações de entrada introduzidas pelo jogador.

Para fazer isso, armazenamos uma cópia das ações de entrada criadas na fila de atraso.


Ou seja, no final do processo no lado do cliente, a imagem se parece com isso:

  1. Aplique as ações de entrada de todos os jogadores ao estado do jogo, pois essas ações de entrada foram recebidas do servidor.
  2. Removemos da fila de atrasos todas as ações de entrada que, de acordo com o servidor, já foram aplicadas ao estado do jogo .
  3. Exclua o estado de latência e redefina-o para que fique exatamente igual ao estado do jogo .
  4. Aplique todas as ações da fila de atraso ao estado de latência .
  5. Com base nos dados do estado do jogo e do estado da latência, renderizamos o jogo para o jogador.

Tudo isso se repete em todas as medidas.

Muito complicado? Não relaxe, isso não é tudo. Para compensar as conexões não confiáveis ​​da Internet, criamos dois mecanismos:

  • Carrapatos perdidos: quando o servidor decide que as ações de entrada serão executadas no ritmo do jogo, se não receber as ações de entrada de um jogador (por exemplo, devido ao aumento no atraso), não esperará, mas dirá a esse cliente: "Não levei em conta suas ações de entrada , tentarei adicioná-las à próxima medida. " Isso é feito para que, devido a problemas com a conexão (ou com o computador) de um jogador, a atualização do mapa não diminua a velocidade para todos os outros. Vale ressaltar que as ações de entrada não são ignoradas, mas simplesmente adiadas.
  • Atraso de ida e volta: o servidor está tentando adivinhar qual é o atraso de ida e volta entre o cliente e o servidor para cada cliente. A cada 5 segundos, se necessário, ele discute com o cliente um novo atraso (dependendo de como a conexão se comportou no passado) e aumenta ou diminui o atraso na transferência de dados.

Por si mesmos, esses mecanismos são bastante simples, mas quando são usados ​​juntos (o que geralmente acontece com problemas de conexão), a lógica do código se torna difícil de gerenciar e com vários casos limítrofes. Além disso, quando esses mecanismos entram em ação , o servidor e a fila de atraso devem implementar corretamente uma Ação de Entrada especial chamada StopMovementInTheNextTick . Devido a isso, com problemas na conexão, o personagem não corre sozinho (por exemplo, sob o trem).

Agora você precisa explicar como a seleção de entidades funciona. Um dos tipos de ação de entrada passados ​​é a alteração no estado da seleção da entidade. Diz a todos que entidade o jogador passou o mouse. Como você pode entender, essa é uma das ações de entrada mais frequentes enviadas pelos clientes; portanto, para economizar largura de banda, a otimizamos para ocupar o mínimo de espaço possível. Isso é implementado da seguinte maneira: ao escolher cada entidade, em vez de preservar as coordenadas absolutas e de alta precisão do mapa, o jogo retém um deslocamento relativo de baixa corrente da escolha anterior. Isso funciona bem porque a seleção do mouse geralmente acontece muito perto da seleção anterior. Por esse motivo, surgem dois requisitos importantes: as ações de entrada nunca devem ser ignoradas e devem ser executadas na ordem correta. Esses requisitos são atendidos para o estado do jogo . Mas como a tarefa do estado Latency é "parecer bom o suficiente" para o player, ele não fica satisfeito com o atraso. O estado de latência não leva em consideração muitos casos fronteiriços associados a pular ciclos e alterar atrasos de ida e volta.

Você já pode adivinhar para onde está indo tudo. Por fim, começamos a ver as causas do problema de megapackage. A raiz do problema é que, ao decidir se deve passar a ação da alteração de seleção, a lógica de seleção da entidade depende do Estado de Latência , e esse estado nem sempre contém as informações corretas. Portanto, um megapackage é gerado assim:

  1. O player tem um problema de conexão.
  2. Os mecanismos para pular os relógios e regular o atraso de ida e volta entram em ação.
  3. Atrasos no estado da fila não levam em consideração esses mecanismos. Isso faz com que algumas ações sejam excluídas prematuramente ou executadas na ordem errada, resultando em um estado de latência incorreto.
  4. O jogador tem um problema com a conexão e ele, para conversar com o servidor, simula até 400 ciclos de clock.
  5. Em cada ciclo do relógio, uma nova ação, alterando a seleção de uma entidade, é gerada e preparada para o envio ao servidor.
  6. O cliente envia ao servidor um megafone de mais de 400 alterações na escolha de entidades (e com outras ações: o estado de tiro, caminhada, etc., também sofreram com esse problema).
  7. O servidor recebe 400 ações de entrada. Como ele não tem permissão para pular uma única ação de entrada, ele ordena a todos os clientes que executem essas ações e as envia pela rede.

A ironia é que um mecanismo projetado para economizar largura de banda do canal criou enormes pacotes de rede como resultado.

Resolvemos esse problema corrigindo todos os casos limítrofes de atualização e suporte à fila de atrasos. Embora demorasse um pouco, no final valia a pena implementar tudo corretamente e não depender de hacks rápidos.

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


All Articles